Skip to main content

jj_cli/
template_builder.rs

1// Copyright 2020-2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::cmp::Ordering;
16use std::collections::HashMap;
17use std::io;
18use std::iter;
19
20use itertools::Itertools as _;
21use jj_lib::backend::Signature;
22use jj_lib::backend::Timestamp;
23use jj_lib::config::ConfigGetResultExt as _;
24use jj_lib::config::ConfigNamePathBuf;
25use jj_lib::config::ConfigValue;
26use jj_lib::content_hash::blake2b_hash;
27use jj_lib::hex_util;
28use jj_lib::op_store::TimestampRange;
29use jj_lib::settings::UserSettings;
30use jj_lib::time_util::DatePattern;
31use serde::Deserialize;
32use serde::de::IntoDeserializer as _;
33
34use crate::config;
35use crate::formatter::FormatRecorder;
36use crate::formatter::Formatter;
37use crate::template_parser;
38use crate::template_parser::BinaryOp;
39use crate::template_parser::ExpressionKind;
40use crate::template_parser::ExpressionNode;
41use crate::template_parser::FunctionCallNode;
42use crate::template_parser::LambdaNode;
43use crate::template_parser::TemplateAliasesMap;
44use crate::template_parser::TemplateDiagnostics;
45use crate::template_parser::TemplateParseError;
46use crate::template_parser::TemplateParseErrorKind;
47use crate::template_parser::TemplateParseResult;
48use crate::template_parser::UnaryOp;
49use crate::templater::AnyTemplateProperty;
50use crate::templater::BoxedAnyProperty;
51use crate::templater::BoxedSerializeProperty;
52use crate::templater::BoxedTemplateProperty;
53use crate::templater::CoalesceTemplate;
54use crate::templater::ConcatTemplate;
55use crate::templater::ConditionalProperty;
56use crate::templater::Email;
57use crate::templater::HyperlinkTemplate;
58use crate::templater::JoinTemplate;
59use crate::templater::LabelTemplate;
60use crate::templater::ListMapProperty;
61use crate::templater::ListPropertyTemplate;
62use crate::templater::Literal;
63use crate::templater::PlainTextFormattedProperty;
64use crate::templater::PropertyPlaceholder;
65use crate::templater::RawEscapeSequenceTemplate;
66use crate::templater::ReformatTemplate;
67use crate::templater::SeparateTemplate;
68use crate::templater::SizeHint;
69use crate::templater::Template;
70use crate::templater::TemplateProperty;
71use crate::templater::TemplatePropertyError;
72use crate::templater::TemplatePropertyExt as _;
73use crate::templater::TemplateRenderer;
74use crate::templater::WrapTemplateProperty;
75use crate::text_util;
76use crate::time_util;
77
78/// Callbacks to build usage-context-specific evaluation objects from AST nodes.
79///
80/// This is used to implement different meanings of `self` or different
81/// globally available functions in the template language depending on the
82/// context in which it is invoked.
83pub trait TemplateLanguage<'a> {
84    type Property: CoreTemplatePropertyVar<'a> + 'a;
85
86    fn settings(&self) -> &UserSettings;
87
88    /// Translates the given global `function` call to a property.
89    ///
90    /// This should be delegated to
91    /// `CoreTemplateBuildFnTable::build_function()`.
92    fn build_function(
93        &self,
94        diagnostics: &mut TemplateDiagnostics,
95        build_ctx: &BuildContext<Self::Property>,
96        function: &FunctionCallNode,
97    ) -> TemplateParseResult<Self::Property>;
98
99    /// Creates a method call thunk for the given `function` of the given
100    /// `property`.
101    fn build_method(
102        &self,
103        diagnostics: &mut TemplateDiagnostics,
104        build_ctx: &BuildContext<Self::Property>,
105        property: Self::Property,
106        function: &FunctionCallNode,
107    ) -> TemplateParseResult<Self::Property>;
108}
109
110/// Implements [`WrapTemplateProperty<'a, O>`] for property types.
111///
112/// - `impl_property_wrappers!(Kind { Foo(Foo), FooList(Vec<Foo>), .. });` to
113///   implement conversion from types `Foo`, `Vec<Foo>`, ...
114/// - `impl_property_wrappers!(<'a> Kind<'a> { .. });` for types with lifetime.
115/// - `impl_property_wrappers!(Kind => Core { .. });` to forward conversion to
116///   `Kind::Core(_)`.
117macro_rules! impl_property_wrappers {
118    ($kind:path $(=> $var:ident)? { $($body:tt)* }) => {
119        $crate::template_builder::_impl_property_wrappers_many!(
120            [], 'static, $kind $(=> $var)?, { $($body)* });
121    };
122    // capture the first lifetime as the lifetime of template objects.
123    (<$a:lifetime $(, $p:lifetime)* $(, $q:ident)*>
124     $kind:path $(=> $var:ident)? { $($body:tt)* }) => {
125        $crate::template_builder::_impl_property_wrappers_many!(
126            [$a, $($p,)* $($q,)*], $a, $kind $(=> $var)?, { $($body)* });
127    };
128}
129
130macro_rules! _impl_property_wrappers_many {
131    // lifetime/type parameters are packed in order to disable zipping.
132    // https://github.com/rust-lang/rust/issues/96184#issuecomment-1294999418
133    ($ps:tt, $a:lifetime, $kind:path, { $( $var:ident($ty:ty), )* }) => {
134        $(
135            $crate::template_builder::_impl_property_wrappers_one!(
136                $ps, $a, $kind, $var, $ty, std::convert::identity);
137        )*
138    };
139    // variant part in body is ignored so the same body can be reused for
140    // implementing forwarding conversion.
141    ($ps:tt, $a:lifetime, $kind:path => $var:ident, { $( $ignored_var:ident($ty:ty), )* }) => {
142        $(
143            $crate::template_builder::_impl_property_wrappers_one!(
144                $ps, $a, $kind, $var, $ty, $crate::templater::WrapTemplateProperty::wrap_property);
145        )*
146    };
147}
148
149macro_rules! _impl_property_wrappers_one {
150    ([$($p:tt)*], $a:lifetime, $kind:path, $var:ident, $ty:ty, $inner:path) => {
151        impl<$($p)*> $crate::templater::WrapTemplateProperty<$a, $ty> for $kind {
152            fn wrap_property(property: $crate::templater::BoxedTemplateProperty<$a, $ty>) -> Self {
153                Self::$var($inner(property))
154            }
155        }
156    };
157}
158
159pub(crate) use _impl_property_wrappers_many;
160pub(crate) use _impl_property_wrappers_one;
161pub(crate) use impl_property_wrappers;
162
163/// Wrapper for the core template property types.
164pub trait CoreTemplatePropertyVar<'a>
165where
166    Self: WrapTemplateProperty<'a, String>,
167    Self: WrapTemplateProperty<'a, Vec<String>>,
168    Self: WrapTemplateProperty<'a, bool>,
169    Self: WrapTemplateProperty<'a, i64>,
170    Self: WrapTemplateProperty<'a, Option<i64>>,
171    Self: WrapTemplateProperty<'a, ConfigValue>,
172    Self: WrapTemplateProperty<'a, Option<ConfigValue>>,
173    Self: WrapTemplateProperty<'a, Signature>,
174    Self: WrapTemplateProperty<'a, Email>,
175    Self: WrapTemplateProperty<'a, SizeHint>,
176    Self: WrapTemplateProperty<'a, Timestamp>,
177    Self: WrapTemplateProperty<'a, TimestampRange>,
178{
179    fn wrap_template(template: Box<dyn Template + 'a>) -> Self;
180    fn wrap_any(property: BoxedAnyProperty<'a>) -> Self;
181    fn wrap_any_list(property: BoxedAnyProperty<'a>) -> Self;
182
183    /// Type name of the property output.
184    fn type_name(&self) -> &'static str;
185
186    fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>>;
187    fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>>;
188    fn try_into_timestamp(self) -> Option<BoxedTemplateProperty<'a, Timestamp>>;
189
190    /// Transforms into a string property by formatting the value if needed.
191    fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>>;
192    fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>>;
193    fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
194
195    /// Transforms into a property that will evaluate to `self == other`.
196    fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>>;
197
198    /// Transforms into a property that will evaluate to an [`Ordering`].
199    fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>>;
200}
201
202pub enum CoreTemplatePropertyKind<'a> {
203    String(BoxedTemplateProperty<'a, String>),
204    StringList(BoxedTemplateProperty<'a, Vec<String>>),
205    Boolean(BoxedTemplateProperty<'a, bool>),
206    Integer(BoxedTemplateProperty<'a, i64>),
207    IntegerOpt(BoxedTemplateProperty<'a, Option<i64>>),
208    ConfigValue(BoxedTemplateProperty<'a, ConfigValue>),
209    ConfigValueOpt(BoxedTemplateProperty<'a, Option<ConfigValue>>),
210    Signature(BoxedTemplateProperty<'a, Signature>),
211    Email(BoxedTemplateProperty<'a, Email>),
212    SizeHint(BoxedTemplateProperty<'a, SizeHint>),
213    Timestamp(BoxedTemplateProperty<'a, Timestamp>),
214    TimestampRange(BoxedTemplateProperty<'a, TimestampRange>),
215
216    // Both TemplateProperty and Template can represent a value to be evaluated
217    // dynamically, which suggests that `Box<dyn Template + 'a>` could be
218    // composed as `Box<dyn TemplateProperty<Output = Box<dyn Template ..`.
219    // However, there's a subtle difference: TemplateProperty is strict on
220    // error, whereas Template is usually lax and prints an error inline. If
221    // `concat(x, y)` were a property returning Template, and if `y` failed to
222    // evaluate, the whole expression would fail. In this example, a partial
223    // evaluation output is more useful. That's one reason why Template isn't
224    // wrapped in a TemplateProperty. Another reason is that the outermost
225    // caller expects a Template, not a TemplateProperty of Template output.
226    Template(Box<dyn Template + 'a>),
227    Any(BoxedAnyProperty<'a>),
228    AnyList(BoxedAnyProperty<'a>),
229}
230
231/// Implements `WrapTemplateProperty<type>` for core property types.
232///
233/// Use `impl_core_property_wrappers!(<'a> Kind<'a> => Core);` to implement
234/// forwarding conversion.
235macro_rules! impl_core_property_wrappers {
236    ($($head:tt)+) => {
237        $crate::template_builder::impl_property_wrappers!($($head)+ {
238            String(String),
239            StringList(Vec<String>),
240            Boolean(bool),
241            Integer(i64),
242            IntegerOpt(Option<i64>),
243            ConfigValue(jj_lib::config::ConfigValue),
244            ConfigValueOpt(Option<jj_lib::config::ConfigValue>),
245            Signature(jj_lib::backend::Signature),
246            Email($crate::templater::Email),
247            SizeHint($crate::templater::SizeHint),
248            Timestamp(jj_lib::backend::Timestamp),
249            TimestampRange(jj_lib::op_store::TimestampRange),
250        });
251    };
252}
253
254pub(crate) use impl_core_property_wrappers;
255
256impl_core_property_wrappers!(<'a> CoreTemplatePropertyKind<'a>);
257
258impl<'a> CoreTemplatePropertyVar<'a> for CoreTemplatePropertyKind<'a> {
259    fn wrap_template(template: Box<dyn Template + 'a>) -> Self {
260        Self::Template(template)
261    }
262
263    fn wrap_any(property: BoxedAnyProperty<'a>) -> Self {
264        Self::Any(property)
265    }
266
267    fn wrap_any_list(property: BoxedAnyProperty<'a>) -> Self {
268        Self::AnyList(property)
269    }
270
271    fn type_name(&self) -> &'static str {
272        match self {
273            Self::String(_) => "String",
274            Self::StringList(_) => "List<String>",
275            Self::Boolean(_) => "Boolean",
276            Self::Integer(_) => "Integer",
277            Self::IntegerOpt(_) => "Option<Integer>",
278            Self::ConfigValue(_) => "ConfigValue",
279            Self::ConfigValueOpt(_) => "Option<ConfigValue>",
280            Self::Signature(_) => "Signature",
281            Self::Email(_) => "Email",
282            Self::SizeHint(_) => "SizeHint",
283            Self::Timestamp(_) => "Timestamp",
284            Self::TimestampRange(_) => "TimestampRange",
285            Self::Template(_) => "Template",
286            Self::Any(_) => "Any",
287            Self::AnyList(_) => "AnyList",
288        }
289    }
290
291    fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
292        match self {
293            Self::String(property) => Some(property.map(|s| !s.is_empty()).into_dyn()),
294            Self::StringList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
295            Self::Boolean(property) => Some(property),
296            Self::Integer(_) => None,
297            Self::IntegerOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
298            Self::ConfigValue(_) => None,
299            Self::ConfigValueOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
300            Self::Signature(_) => None,
301            Self::Email(property) => Some(property.map(|e| !e.0.is_empty()).into_dyn()),
302            Self::SizeHint(_) => None,
303            Self::Timestamp(_) => None,
304            Self::TimestampRange(_) => None,
305            // Template and AnyList types could also be evaluated to boolean,
306            // but it's less likely to apply label() or .map() and use the
307            // result as conditional.
308            Self::Template(_) => None,
309            Self::Any(_) => None,
310            Self::AnyList(_) => None,
311        }
312    }
313
314    fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
315        match self {
316            Self::Integer(property) => Some(property),
317            Self::IntegerOpt(property) => Some(property.try_unwrap("Integer").into_dyn()),
318            _ => None,
319        }
320    }
321
322    fn try_into_timestamp(self) -> Option<BoxedTemplateProperty<'a, Timestamp>> {
323        match self {
324            Self::Timestamp(property) => Some(property),
325            _ => None,
326        }
327    }
328
329    fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>> {
330        match self {
331            Self::String(property) => Some(property),
332            _ => {
333                let template = self.try_into_template()?;
334                Some(PlainTextFormattedProperty::new(template).into_dyn())
335            }
336        }
337    }
338
339    fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
340        match self {
341            Self::String(property) => Some(property.into_serialize()),
342            Self::StringList(property) => Some(property.into_serialize()),
343            Self::Boolean(property) => Some(property.into_serialize()),
344            Self::Integer(property) => Some(property.into_serialize()),
345            Self::IntegerOpt(property) => Some(property.into_serialize()),
346            Self::ConfigValue(property) => {
347                Some(property.map(config::to_serializable_value).into_serialize())
348            }
349            Self::ConfigValueOpt(property) => Some(
350                property
351                    .map(|opt| opt.map(config::to_serializable_value))
352                    .into_serialize(),
353            ),
354            Self::Signature(property) => Some(property.into_serialize()),
355            Self::Email(property) => Some(property.into_serialize()),
356            Self::SizeHint(property) => Some(property.into_serialize()),
357            Self::Timestamp(property) => Some(property.into_serialize()),
358            Self::TimestampRange(property) => Some(property.into_serialize()),
359            Self::Template(_) => None,
360            Self::Any(property) => property.try_into_serialize(),
361            Self::AnyList(property) => property.try_into_serialize(),
362        }
363    }
364
365    fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
366        match self {
367            Self::String(property) => Some(property.into_template()),
368            Self::StringList(property) => Some(property.into_template()),
369            Self::Boolean(property) => Some(property.into_template()),
370            Self::Integer(property) => Some(property.into_template()),
371            Self::IntegerOpt(property) => Some(property.into_template()),
372            Self::ConfigValue(property) => Some(property.into_template()),
373            Self::ConfigValueOpt(property) => Some(property.into_template()),
374            Self::Signature(property) => Some(property.into_template()),
375            Self::Email(property) => Some(property.into_template()),
376            Self::SizeHint(_) => None,
377            Self::Timestamp(property) => Some(property.into_template()),
378            Self::TimestampRange(property) => Some(property.into_template()),
379            Self::Template(template) => Some(template),
380            Self::Any(property) => property.try_into_template(),
381            Self::AnyList(property) => property.try_into_template(),
382        }
383    }
384
385    fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
386        match (self, other) {
387            (Self::String(lhs), Self::String(rhs)) => {
388                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
389            }
390            (Self::String(lhs), Self::Email(rhs)) => {
391                Some((lhs, rhs).map(|(l, r)| l == r.0).into_dyn())
392            }
393            (Self::Boolean(lhs), Self::Boolean(rhs)) => {
394                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
395            }
396            (Self::Integer(lhs), Self::Integer(rhs)) => {
397                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
398            }
399            (Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
400                Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn())
401            }
402            (Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
403                Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn())
404            }
405            (Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
406                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
407            }
408            (Self::Email(lhs), Self::Email(rhs)) => {
409                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
410            }
411            (Self::Email(lhs), Self::String(rhs)) => {
412                Some((lhs, rhs).map(|(l, r)| l.0 == r).into_dyn())
413            }
414            (Self::String(_), _) => None,
415            (Self::StringList(_), _) => None,
416            (Self::Boolean(_), _) => None,
417            (Self::Integer(_), _) => None,
418            (Self::IntegerOpt(_), _) => None,
419            (Self::ConfigValue(_), _) => None,
420            (Self::ConfigValueOpt(_), _) => None,
421            (Self::Signature(_), _) => None,
422            (Self::Email(_), _) => None,
423            (Self::SizeHint(_), _) => None,
424            (Self::Timestamp(_), _) => None,
425            (Self::TimestampRange(_), _) => None,
426            (Self::Template(_), _) => None,
427            (Self::Any(_), _) => None,
428            (Self::AnyList(_), _) => None,
429        }
430    }
431
432    fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
433        match (self, other) {
434            (Self::Integer(lhs), Self::Integer(rhs)) => {
435                Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
436            }
437            (Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
438                Some((lhs, rhs).map(|(l, r)| Some(l).cmp(&r)).into_dyn())
439            }
440            (Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
441                Some((lhs, rhs).map(|(l, r)| l.cmp(&Some(r))).into_dyn())
442            }
443            (Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
444                Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
445            }
446            (Self::String(_), _) => None,
447            (Self::StringList(_), _) => None,
448            (Self::Boolean(_), _) => None,
449            (Self::Integer(_), _) => None,
450            (Self::IntegerOpt(_), _) => None,
451            (Self::ConfigValue(_), _) => None,
452            (Self::ConfigValueOpt(_), _) => None,
453            (Self::Signature(_), _) => None,
454            (Self::Email(_), _) => None,
455            (Self::SizeHint(_), _) => None,
456            (Self::Timestamp(_), _) => None,
457            (Self::TimestampRange(_), _) => None,
458            (Self::Template(_), _) => None,
459            (Self::Any(_), _) => None,
460            (Self::AnyList(_), _) => None,
461        }
462    }
463}
464
465/// Function that translates global function call node.
466// The lifetime parameter 'a could be replaced with for<'a> to keep the method
467// table away from a certain lifetime. That's technically more correct, but I
468// couldn't find an easy way to expand that to the core template methods, which
469// are defined for L: TemplateLanguage<'a>. That's why the build fn table is
470// bound to a named lifetime, and therefore can't be cached statically.
471pub type TemplateBuildFunctionFn<'a, L, P> =
472    fn(&L, &mut TemplateDiagnostics, &BuildContext<P>, &FunctionCallNode) -> TemplateParseResult<P>;
473
474type BuildMethodFn<'a, L, T, P> = fn(
475    &L,
476    &mut TemplateDiagnostics,
477    &BuildContext<P>,
478    T,
479    &FunctionCallNode,
480) -> TemplateParseResult<P>;
481
482/// Function that translates method call node of self type `T`.
483pub type TemplateBuildMethodFn<'a, L, T, P> = BuildMethodFn<'a, L, BoxedTemplateProperty<'a, T>, P>;
484
485/// Function that translates method call node of `Template`.
486pub type BuildTemplateMethodFn<'a, L, P> = BuildMethodFn<'a, L, Box<dyn Template + 'a>, P>;
487
488/// Function that translates method call node of `Any*`.
489pub type BuildAnyMethodFn<'a, L, P> = BuildMethodFn<'a, L, BoxedAnyProperty<'a>, P>;
490
491/// Table of functions that translate global function call node.
492pub type TemplateBuildFunctionFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
493    HashMap<&'static str, TemplateBuildFunctionFn<'a, L, P>>;
494
495/// Table of functions that translate method call node of self type `T`.
496pub type TemplateBuildMethodFnMap<'a, L, T, P = <L as TemplateLanguage<'a>>::Property> =
497    HashMap<&'static str, TemplateBuildMethodFn<'a, L, T, P>>;
498
499/// Table of functions that translate method call node of `Template`.
500pub type BuildTemplateMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
501    HashMap<&'static str, BuildTemplateMethodFn<'a, L, P>>;
502
503/// Table of functions that translate method call node of `Any*`.
504pub type BuildAnyMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
505    HashMap<&'static str, BuildAnyMethodFn<'a, L, P>>;
506
507/// Symbol table of functions and methods available in the core template.
508pub struct CoreTemplateBuildFnTable<'a, L: ?Sized, P = <L as TemplateLanguage<'a>>::Property> {
509    pub functions: TemplateBuildFunctionFnMap<'a, L, P>,
510    pub string_methods: TemplateBuildMethodFnMap<'a, L, String, P>,
511    pub string_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<String>, P>,
512    pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool, P>,
513    pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64, P>,
514    pub config_value_methods: TemplateBuildMethodFnMap<'a, L, ConfigValue, P>,
515    pub email_methods: TemplateBuildMethodFnMap<'a, L, Email, P>,
516    pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature, P>,
517    pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint, P>,
518    pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp, P>,
519    pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange, P>,
520    pub template_methods: BuildTemplateMethodFnMap<'a, L, P>,
521    pub any_methods: BuildAnyMethodFnMap<'a, L, P>,
522    pub any_list_methods: BuildAnyMethodFnMap<'a, L, P>,
523}
524
525pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
526    for (name, function) in extension {
527        if base.insert(name, function).is_some() {
528            panic!("Conflicting template definitions for '{name}' function");
529        }
530    }
531}
532
533impl<L: ?Sized, P> CoreTemplateBuildFnTable<'_, L, P> {
534    pub fn empty() -> Self {
535        Self {
536            functions: HashMap::new(),
537            string_methods: HashMap::new(),
538            string_list_methods: HashMap::new(),
539            boolean_methods: HashMap::new(),
540            integer_methods: HashMap::new(),
541            config_value_methods: HashMap::new(),
542            signature_methods: HashMap::new(),
543            email_methods: HashMap::new(),
544            size_hint_methods: HashMap::new(),
545            timestamp_methods: HashMap::new(),
546            timestamp_range_methods: HashMap::new(),
547            template_methods: HashMap::new(),
548            any_methods: HashMap::new(),
549            any_list_methods: HashMap::new(),
550        }
551    }
552
553    pub fn merge(&mut self, other: Self) {
554        let Self {
555            functions,
556            string_methods,
557            string_list_methods,
558            boolean_methods,
559            integer_methods,
560            config_value_methods,
561            signature_methods,
562            email_methods,
563            size_hint_methods,
564            timestamp_methods,
565            timestamp_range_methods,
566            template_methods,
567            any_methods,
568            any_list_methods,
569        } = other;
570
571        merge_fn_map(&mut self.functions, functions);
572        merge_fn_map(&mut self.string_methods, string_methods);
573        merge_fn_map(&mut self.string_list_methods, string_list_methods);
574        merge_fn_map(&mut self.boolean_methods, boolean_methods);
575        merge_fn_map(&mut self.integer_methods, integer_methods);
576        merge_fn_map(&mut self.config_value_methods, config_value_methods);
577        merge_fn_map(&mut self.signature_methods, signature_methods);
578        merge_fn_map(&mut self.email_methods, email_methods);
579        merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
580        merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
581        merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
582        merge_fn_map(&mut self.template_methods, template_methods);
583        merge_fn_map(&mut self.any_methods, any_methods);
584        merge_fn_map(&mut self.any_list_methods, any_list_methods);
585    }
586}
587
588impl<'a, L> CoreTemplateBuildFnTable<'a, L, L::Property>
589where
590    L: TemplateLanguage<'a> + ?Sized,
591{
592    /// Creates new symbol table containing the builtin functions and methods.
593    pub fn builtin() -> Self {
594        Self {
595            functions: builtin_functions(),
596            string_methods: builtin_string_methods(),
597            string_list_methods: builtin_formattable_list_methods(),
598            boolean_methods: HashMap::new(),
599            integer_methods: HashMap::new(),
600            config_value_methods: builtin_config_value_methods(),
601            signature_methods: builtin_signature_methods(),
602            email_methods: builtin_email_methods(),
603            size_hint_methods: builtin_size_hint_methods(),
604            timestamp_methods: builtin_timestamp_methods(),
605            timestamp_range_methods: builtin_timestamp_range_methods(),
606            template_methods: HashMap::new(),
607            any_methods: HashMap::new(),
608            any_list_methods: builtin_any_list_methods(),
609        }
610    }
611
612    /// Translates the function call node `function` by using this symbol table.
613    pub fn build_function(
614        &self,
615        language: &L,
616        diagnostics: &mut TemplateDiagnostics,
617        build_ctx: &BuildContext<L::Property>,
618        function: &FunctionCallNode,
619    ) -> TemplateParseResult<L::Property> {
620        let table = &self.functions;
621        let build = template_parser::lookup_function(table, function)?;
622        build(language, diagnostics, build_ctx, function)
623    }
624
625    /// Applies the method call node `function` to the given `property` by using
626    /// this symbol table.
627    pub fn build_method(
628        &self,
629        language: &L,
630        diagnostics: &mut TemplateDiagnostics,
631        build_ctx: &BuildContext<L::Property>,
632        property: CoreTemplatePropertyKind<'a>,
633        function: &FunctionCallNode,
634    ) -> TemplateParseResult<L::Property> {
635        let type_name = property.type_name();
636        match property {
637            CoreTemplatePropertyKind::String(property) => {
638                let table = &self.string_methods;
639                let build = template_parser::lookup_method(type_name, table, function)?;
640                build(language, diagnostics, build_ctx, property, function)
641            }
642            CoreTemplatePropertyKind::StringList(property) => {
643                let table = &self.string_list_methods;
644                let build = template_parser::lookup_method(type_name, table, function)?;
645                build(language, diagnostics, build_ctx, property, function)
646            }
647            CoreTemplatePropertyKind::Boolean(property) => {
648                let table = &self.boolean_methods;
649                let build = template_parser::lookup_method(type_name, table, function)?;
650                build(language, diagnostics, build_ctx, property, function)
651            }
652            CoreTemplatePropertyKind::Integer(property) => {
653                let table = &self.integer_methods;
654                let build = template_parser::lookup_method(type_name, table, function)?;
655                build(language, diagnostics, build_ctx, property, function)
656            }
657            CoreTemplatePropertyKind::IntegerOpt(property) => {
658                let type_name = "Integer";
659                let table = &self.integer_methods;
660                let build = template_parser::lookup_method(type_name, table, function)?;
661                let inner_property = property.try_unwrap(type_name).into_dyn();
662                build(language, diagnostics, build_ctx, inner_property, function)
663            }
664            CoreTemplatePropertyKind::ConfigValue(property) => {
665                let table = &self.config_value_methods;
666                let build = template_parser::lookup_method(type_name, table, function)?;
667                build(language, diagnostics, build_ctx, property, function)
668            }
669            CoreTemplatePropertyKind::ConfigValueOpt(property) => {
670                let type_name = "ConfigValue";
671                let table = &self.config_value_methods;
672                let build = template_parser::lookup_method(type_name, table, function)?;
673                let inner_property = property.try_unwrap(type_name).into_dyn();
674                build(language, diagnostics, build_ctx, inner_property, function)
675            }
676            CoreTemplatePropertyKind::Signature(property) => {
677                let table = &self.signature_methods;
678                let build = template_parser::lookup_method(type_name, table, function)?;
679                build(language, diagnostics, build_ctx, property, function)
680            }
681            CoreTemplatePropertyKind::Email(property) => {
682                let table = &self.email_methods;
683                let build = template_parser::lookup_method(type_name, table, function)?;
684                build(language, diagnostics, build_ctx, property, function)
685            }
686            CoreTemplatePropertyKind::SizeHint(property) => {
687                let table = &self.size_hint_methods;
688                let build = template_parser::lookup_method(type_name, table, function)?;
689                build(language, diagnostics, build_ctx, property, function)
690            }
691            CoreTemplatePropertyKind::Timestamp(property) => {
692                let table = &self.timestamp_methods;
693                let build = template_parser::lookup_method(type_name, table, function)?;
694                build(language, diagnostics, build_ctx, property, function)
695            }
696            CoreTemplatePropertyKind::TimestampRange(property) => {
697                let table = &self.timestamp_range_methods;
698                let build = template_parser::lookup_method(type_name, table, function)?;
699                build(language, diagnostics, build_ctx, property, function)
700            }
701            CoreTemplatePropertyKind::Template(template) => {
702                let table = &self.template_methods;
703                let build = template_parser::lookup_method(type_name, table, function)?;
704                build(language, diagnostics, build_ctx, template, function)
705            }
706            CoreTemplatePropertyKind::Any(property) => {
707                let table = &self.any_methods;
708                let build = template_parser::lookup_method(type_name, table, function)?;
709                build(language, diagnostics, build_ctx, property, function)
710            }
711            CoreTemplatePropertyKind::AnyList(property) => {
712                let table = &self.any_list_methods;
713                let build = template_parser::lookup_method(type_name, table, function)?;
714                build(language, diagnostics, build_ctx, property, function)
715            }
716        }
717    }
718}
719
720/// Opaque struct that represents a template value.
721pub struct Expression<P> {
722    property: P,
723    labels: Vec<String>,
724}
725
726impl<P> Expression<P> {
727    fn unlabeled(property: P) -> Self {
728        let labels = vec![];
729        Self { property, labels }
730    }
731
732    fn with_label(property: P, label: impl Into<String>) -> Self {
733        let labels = vec![label.into()];
734        Self { property, labels }
735    }
736}
737
738impl<'a, P: CoreTemplatePropertyVar<'a>> Expression<P> {
739    pub fn type_name(&self) -> &'static str {
740        self.property.type_name()
741    }
742
743    pub fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
744        self.property.try_into_boolean()
745    }
746
747    pub fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
748        self.property.try_into_integer()
749    }
750
751    pub fn try_into_timestamp(self) -> Option<BoxedTemplateProperty<'a, Timestamp>> {
752        self.property.try_into_timestamp()
753    }
754
755    pub fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>> {
756        self.property.try_into_stringify()
757    }
758
759    pub fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
760        self.property.try_into_serialize()
761    }
762
763    pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
764        let template = self.property.try_into_template()?;
765        if self.labels.is_empty() {
766            Some(template)
767        } else {
768            Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
769        }
770    }
771
772    pub fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
773        self.property.try_into_eq(other.property)
774    }
775
776    pub fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
777        self.property.try_into_cmp(other.property)
778    }
779}
780
781impl<'a, P: CoreTemplatePropertyVar<'a>> AnyTemplateProperty<'a> for Expression<P> {
782    fn try_into_serialize(self: Box<Self>) -> Option<BoxedSerializeProperty<'a>> {
783        (*self).try_into_serialize()
784    }
785
786    fn try_into_template(self: Box<Self>) -> Option<Box<dyn Template + 'a>> {
787        (*self).try_into_template()
788    }
789
790    fn try_join(self: Box<Self>, _: Box<dyn Template + 'a>) -> Option<Box<dyn Template + 'a>> {
791        None
792    }
793}
794
795/// Environment (locals and self) in a stack frame.
796pub struct BuildContext<'i, P> {
797    /// Map of functions to create `L::Property`.
798    local_variables: HashMap<&'i str, &'i dyn Fn() -> P>,
799    /// Function to create `L::Property` representing `self`.
800    ///
801    /// This could be `local_variables["self"]`, but keyword lookup shouldn't be
802    /// overridden by a user-defined `self` variable.
803    self_variable: &'i dyn Fn() -> P,
804}
805
806fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
807    language: &L,
808    diagnostics: &mut TemplateDiagnostics,
809    build_ctx: &BuildContext<L::Property>,
810    name: &str,
811    name_span: pest::Span<'_>,
812) -> TemplateParseResult<L::Property> {
813    // Keyword is a 0-ary method on the "self" property
814    let self_property = (build_ctx.self_variable)();
815    let function = FunctionCallNode {
816        name,
817        name_span,
818        args: vec![],
819        keyword_args: vec![],
820        args_span: name_span.end_pos().span(&name_span.end_pos()),
821    };
822    language
823        .build_method(diagnostics, build_ctx, self_property, &function)
824        .map_err(|err| match err.kind() {
825            TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
826                let kind = TemplateParseErrorKind::NoSuchKeyword {
827                    name: name.to_owned(),
828                    // TODO: filter methods by arity?
829                    candidates: candidates.clone(),
830                };
831                TemplateParseError::with_span(kind, name_span)
832            }
833            // Since keyword is a 0-ary method, any argument errors mean there's
834            // no such keyword.
835            TemplateParseErrorKind::InvalidArguments { .. } => {
836                let kind = TemplateParseErrorKind::NoSuchKeyword {
837                    name: name.to_owned(),
838                    // TODO: might be better to phrase the error differently
839                    candidates: vec![format!("self.{name}(..)")],
840                };
841                TemplateParseError::with_span(kind, name_span)
842            }
843            // The keyword function may fail with the other reasons.
844            _ => err,
845        })
846}
847
848fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
849    language: &L,
850    diagnostics: &mut TemplateDiagnostics,
851    build_ctx: &BuildContext<L::Property>,
852    op: UnaryOp,
853    arg_node: &ExpressionNode,
854) -> TemplateParseResult<L::Property> {
855    match op {
856        UnaryOp::LogicalNot => {
857            let arg = expect_boolean_expression(language, diagnostics, build_ctx, arg_node)?;
858            Ok(arg.map(|v| !v).into_dyn_wrapped())
859        }
860        UnaryOp::Negate => {
861            let arg = expect_integer_expression(language, diagnostics, build_ctx, arg_node)?;
862            let out = arg.and_then(|v| {
863                v.checked_neg()
864                    .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
865            });
866            Ok(out.into_dyn_wrapped())
867        }
868    }
869}
870
871fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
872    language: &L,
873    diagnostics: &mut TemplateDiagnostics,
874    build_ctx: &BuildContext<L::Property>,
875    op: BinaryOp,
876    lhs_node: &ExpressionNode,
877    rhs_node: &ExpressionNode,
878    span: pest::Span<'_>,
879) -> TemplateParseResult<L::Property> {
880    match op {
881        BinaryOp::LogicalOr => {
882            let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
883            let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
884            let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
885            Ok(out.into_dyn_wrapped())
886        }
887        BinaryOp::LogicalAnd => {
888            let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
889            let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
890            let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
891            Ok(out.into_dyn_wrapped())
892        }
893        BinaryOp::Eq | BinaryOp::Ne => {
894            let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
895            let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
896            let lty = lhs.type_name();
897            let rty = rhs.type_name();
898            let eq = lhs.try_into_eq(rhs).ok_or_else(|| {
899                let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
900                TemplateParseError::expression(message, span)
901            })?;
902            let out = match op {
903                BinaryOp::Eq => eq.into_dyn(),
904                BinaryOp::Ne => eq.map(|eq| !eq).into_dyn(),
905                _ => unreachable!(),
906            };
907            Ok(L::Property::wrap_property(out))
908        }
909        BinaryOp::Ge | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Lt => {
910            let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
911            let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
912            let lty = lhs.type_name();
913            let rty = rhs.type_name();
914            let cmp = lhs.try_into_cmp(rhs).ok_or_else(|| {
915                let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
916                TemplateParseError::expression(message, span)
917            })?;
918            let out = match op {
919                BinaryOp::Ge => cmp.map(|ordering| ordering.is_ge()).into_dyn(),
920                BinaryOp::Gt => cmp.map(|ordering| ordering.is_gt()).into_dyn(),
921                BinaryOp::Le => cmp.map(|ordering| ordering.is_le()).into_dyn(),
922                BinaryOp::Lt => cmp.map(|ordering| ordering.is_lt()).into_dyn(),
923                _ => unreachable!(),
924            };
925            Ok(L::Property::wrap_property(out))
926        }
927        BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => {
928            let lhs = expect_integer_expression(language, diagnostics, build_ctx, lhs_node)?;
929            let rhs = expect_integer_expression(language, diagnostics, build_ctx, rhs_node)?;
930            let build = |op: fn(i64, i64) -> Option<i64>, msg: fn(i64) -> &'static str| {
931                (lhs, rhs).and_then(move |(l, r)| {
932                    op(l, r).ok_or_else(|| TemplatePropertyError(msg(r).into()))
933                })
934            };
935            let out = match op {
936                BinaryOp::Add => build(i64::checked_add, |_| "Attempt to add with overflow"),
937                BinaryOp::Sub => build(i64::checked_sub, |_| "Attempt to subtract with overflow"),
938                BinaryOp::Mul => build(i64::checked_mul, |_| "Attempt to multiply with overflow"),
939                BinaryOp::Div => build(i64::checked_div, |r| {
940                    if r == 0 {
941                        "Attempt to divide by zero"
942                    } else {
943                        "Attempt to divide with overflow"
944                    }
945                }),
946                BinaryOp::Rem => build(i64::checked_rem, |r| {
947                    if r == 0 {
948                        "Attempt to divide by zero"
949                    } else {
950                        "Attempt to divide with overflow"
951                    }
952                }),
953                _ => unreachable!(),
954            };
955            Ok(out.into_dyn_wrapped())
956        }
957    }
958}
959
960fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
961-> TemplateBuildMethodFnMap<'a, L, String> {
962    // Not using maplit::hashmap!{} or custom declarative macro here because
963    // code completion inside macro is quite restricted.
964    let mut map = TemplateBuildMethodFnMap::<L, String>::new();
965    map.insert(
966        "len",
967        |_language, _diagnostics, _build_ctx, self_property, function| {
968            function.expect_no_arguments()?;
969            let out_property = self_property.and_then(|s| Ok(i64::try_from(s.len())?));
970            Ok(out_property.into_dyn_wrapped())
971        },
972    );
973    map.insert(
974        "contains",
975        |language, diagnostics, build_ctx, self_property, function| {
976            let [needle_node] = function.expect_exact_arguments()?;
977            // TODO: or .try_into_string() to disable implicit type cast?
978            let needle_property =
979                expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
980            let out_property = (self_property, needle_property)
981                .map(|(haystack, needle)| haystack.contains(&needle));
982            Ok(out_property.into_dyn_wrapped())
983        },
984    );
985    map.insert(
986        "match",
987        |_language, _diagnostics, _build_ctx, self_property, function| {
988            let [needle_node] = function.expect_exact_arguments()?;
989            let needle = template_parser::expect_string_pattern(needle_node)?;
990            let regex = needle.to_regex();
991
992            let out_property = self_property.and_then(move |haystack| {
993                if let Some(m) = regex.find(haystack.as_bytes()) {
994                    Ok(str::from_utf8(m.as_bytes())?.to_owned())
995                } else {
996                    // We don't have optional strings, so empty string is the
997                    // right null value.
998                    Ok(String::new())
999                }
1000            });
1001            Ok(out_property.into_dyn_wrapped())
1002        },
1003    );
1004    map.insert(
1005        "starts_with",
1006        |language, diagnostics, build_ctx, self_property, function| {
1007            let [needle_node] = function.expect_exact_arguments()?;
1008            let needle_property =
1009                expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1010            let out_property = (self_property, needle_property)
1011                .map(|(haystack, needle)| haystack.starts_with(&needle));
1012            Ok(out_property.into_dyn_wrapped())
1013        },
1014    );
1015    map.insert(
1016        "ends_with",
1017        |language, diagnostics, build_ctx, self_property, function| {
1018            let [needle_node] = function.expect_exact_arguments()?;
1019            let needle_property =
1020                expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1021            let out_property = (self_property, needle_property)
1022                .map(|(haystack, needle)| haystack.ends_with(&needle));
1023            Ok(out_property.into_dyn_wrapped())
1024        },
1025    );
1026    map.insert(
1027        "remove_prefix",
1028        |language, diagnostics, build_ctx, self_property, function| {
1029            let [needle_node] = function.expect_exact_arguments()?;
1030            let needle_property =
1031                expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1032            let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1033                haystack
1034                    .strip_prefix(&needle)
1035                    .map(ToOwned::to_owned)
1036                    .unwrap_or(haystack)
1037            });
1038            Ok(out_property.into_dyn_wrapped())
1039        },
1040    );
1041    map.insert(
1042        "remove_suffix",
1043        |language, diagnostics, build_ctx, self_property, function| {
1044            let [needle_node] = function.expect_exact_arguments()?;
1045            let needle_property =
1046                expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1047            let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1048                haystack
1049                    .strip_suffix(&needle)
1050                    .map(ToOwned::to_owned)
1051                    .unwrap_or(haystack)
1052            });
1053            Ok(out_property.into_dyn_wrapped())
1054        },
1055    );
1056    map.insert(
1057        "trim",
1058        |_language, _diagnostics, _build_ctx, self_property, function| {
1059            function.expect_no_arguments()?;
1060            let out_property = self_property.map(|s| s.trim().to_owned());
1061            Ok(out_property.into_dyn_wrapped())
1062        },
1063    );
1064    map.insert(
1065        "trim_start",
1066        |_language, _diagnostics, _build_ctx, self_property, function| {
1067            function.expect_no_arguments()?;
1068            let out_property = self_property.map(|s| s.trim_start().to_owned());
1069            Ok(out_property.into_dyn_wrapped())
1070        },
1071    );
1072    map.insert(
1073        "trim_end",
1074        |_language, _diagnostics, _build_ctx, self_property, function| {
1075            function.expect_no_arguments()?;
1076            let out_property = self_property.map(|s| s.trim_end().to_owned());
1077            Ok(out_property.into_dyn_wrapped())
1078        },
1079    );
1080    map.insert(
1081        "substr",
1082        |language, diagnostics, build_ctx, self_property, function| {
1083            let ([start_idx], [end_idx]) = function.expect_arguments()?;
1084            let start_idx_property =
1085                expect_isize_expression(language, diagnostics, build_ctx, start_idx)?;
1086            let end_idx_property = if let Some(end_idx) = end_idx {
1087                Some(expect_isize_expression(
1088                    language,
1089                    diagnostics,
1090                    build_ctx,
1091                    end_idx,
1092                )?)
1093            } else {
1094                None
1095            };
1096            let out_property = (self_property, start_idx_property, end_idx_property).map(
1097                |(s, start_idx, end_idx)| {
1098                    let start_idx = string_index_to_char_boundary(&s, start_idx);
1099                    let end_idx = if let Some(idx) = end_idx {
1100                        string_index_to_char_boundary(&s, idx)
1101                    } else {
1102                        s.len()
1103                    };
1104                    s.get(start_idx..end_idx).unwrap_or_default().to_owned()
1105                },
1106            );
1107            Ok(out_property.into_dyn_wrapped())
1108        },
1109    );
1110    map.insert(
1111        "first_line",
1112        |_language, _diagnostics, _build_ctx, self_property, function| {
1113            function.expect_no_arguments()?;
1114            let out_property =
1115                self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
1116            Ok(out_property.into_dyn_wrapped())
1117        },
1118    );
1119    map.insert(
1120        "lines",
1121        |_language, _diagnostics, _build_ctx, self_property, function| {
1122            function.expect_no_arguments()?;
1123            let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect_vec());
1124            Ok(out_property.into_dyn_wrapped())
1125        },
1126    );
1127    map.insert(
1128        "split",
1129        |language, diagnostics, build_ctx, self_property, function| {
1130            let ([separator_node], [limit_node]) = function.expect_arguments()?;
1131            let pattern = template_parser::expect_string_pattern(separator_node)?;
1132            let regex = pattern.to_regex();
1133
1134            if let Some(limit_node) = limit_node {
1135                let limit_property =
1136                    expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1137                let out_property =
1138                    (self_property, limit_property).and_then(move |(haystack, limit)| {
1139                        let haystack_bytes = haystack.as_bytes();
1140                        let parts: Vec<_> = regex
1141                            .splitn(haystack_bytes, limit)
1142                            .map(|part| str::from_utf8(part).map(|s| s.to_owned()))
1143                            .try_collect()?;
1144                        Ok(parts)
1145                    });
1146                Ok(out_property.into_dyn_wrapped())
1147            } else {
1148                let out_property = self_property.and_then(move |haystack| {
1149                    let haystack_bytes = haystack.as_bytes();
1150                    let parts: Vec<_> = regex
1151                        .split(haystack_bytes)
1152                        .map(|part| str::from_utf8(part).map(|s| s.to_owned()))
1153                        .try_collect()?;
1154                    Ok(parts)
1155                });
1156                Ok(out_property.into_dyn_wrapped())
1157            }
1158        },
1159    );
1160    map.insert(
1161        "upper",
1162        |_language, _diagnostics, _build_ctx, self_property, function| {
1163            function.expect_no_arguments()?;
1164            let out_property = self_property.map(|s| s.to_uppercase());
1165            Ok(out_property.into_dyn_wrapped())
1166        },
1167    );
1168    map.insert(
1169        "lower",
1170        |_language, _diagnostics, _build_ctx, self_property, function| {
1171            function.expect_no_arguments()?;
1172            let out_property = self_property.map(|s| s.to_lowercase());
1173            Ok(out_property.into_dyn_wrapped())
1174        },
1175    );
1176    map.insert(
1177        "escape_json",
1178        |_language, _diagnostics, _build_ctx, self_property, function| {
1179            function.expect_no_arguments()?;
1180            let out_property = self_property.map(|s| serde_json::to_string(&s).unwrap());
1181            Ok(out_property.into_dyn_wrapped())
1182        },
1183    );
1184    map.insert(
1185        "replace",
1186        |language, diagnostics, build_ctx, self_property, function| {
1187            let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
1188            let pattern = template_parser::expect_string_pattern(pattern_node)?;
1189            let replacement_property =
1190                expect_stringify_expression(language, diagnostics, build_ctx, replacement_node)?;
1191
1192            let regex = pattern.to_regex();
1193
1194            if let Some(limit_node) = limit_node {
1195                let limit_property =
1196                    expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1197                let out_property = (self_property, replacement_property, limit_property).and_then(
1198                    move |(haystack, replacement, limit)| {
1199                        if limit == 0 {
1200                            // We need to special-case zero because regex.replacen(_, 0, _) replaces
1201                            // all occurrences, and we want zero to mean no occurrences are
1202                            // replaced.
1203                            Ok(haystack)
1204                        } else {
1205                            let haystack_bytes = haystack.as_bytes();
1206                            let replace_bytes = replacement.as_bytes();
1207                            let result = regex.replacen(haystack_bytes, limit, replace_bytes);
1208                            Ok(str::from_utf8(&result)?.to_owned())
1209                        }
1210                    },
1211                );
1212                Ok(out_property.into_dyn_wrapped())
1213            } else {
1214                let out_property = (self_property, replacement_property).and_then(
1215                    move |(haystack, replacement)| {
1216                        let haystack_bytes = haystack.as_bytes();
1217                        let replace_bytes = replacement.as_bytes();
1218                        let result = regex.replace_all(haystack_bytes, replace_bytes);
1219                        Ok(str::from_utf8(&result)?.to_owned())
1220                    },
1221                );
1222                Ok(out_property.into_dyn_wrapped())
1223            }
1224        },
1225    );
1226    map
1227}
1228
1229/// Clamps and aligns the given index `i` to char boundary.
1230///
1231/// Negative index counts from the end. If the index isn't at a char boundary,
1232/// it will be rounded towards 0 (left or right depending on the sign.)
1233fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
1234    // TODO: use floor/ceil_char_boundary() if get stabilized
1235    let magnitude = i.unsigned_abs();
1236    if i < 0 {
1237        let p = s.len().saturating_sub(magnitude);
1238        (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
1239    } else {
1240        let p = magnitude.min(s.len());
1241        (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
1242    }
1243}
1244
1245fn builtin_config_value_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1246-> TemplateBuildMethodFnMap<'a, L, ConfigValue> {
1247    fn extract<'de, T: Deserialize<'de>>(value: ConfigValue) -> Result<T, TemplatePropertyError> {
1248        T::deserialize(value.into_deserializer())
1249            // map to err.message() because TomlError appends newline to it
1250            .map_err(|err| TemplatePropertyError(err.message().into()))
1251    }
1252
1253    // Not using maplit::hashmap!{} or custom declarative macro here because
1254    // code completion inside macro is quite restricted.
1255    let mut map = TemplateBuildMethodFnMap::<L, ConfigValue>::new();
1256    // These methods are called "as_<type>", not "to_<type>" to clarify that
1257    // they'll never convert types (e.g. integer to string.) Since templater
1258    // doesn't provide binding syntax, there's no need to distinguish between
1259    // reference and consuming access.
1260    map.insert(
1261        "as_boolean",
1262        |_language, _diagnostics, _build_ctx, self_property, function| {
1263            function.expect_no_arguments()?;
1264            let out_property = self_property.and_then(extract::<bool>);
1265            Ok(out_property.into_dyn_wrapped())
1266        },
1267    );
1268    map.insert(
1269        "as_integer",
1270        |_language, _diagnostics, _build_ctx, self_property, function| {
1271            function.expect_no_arguments()?;
1272            let out_property = self_property.and_then(extract::<i64>);
1273            Ok(out_property.into_dyn_wrapped())
1274        },
1275    );
1276    map.insert(
1277        "as_string",
1278        |_language, _diagnostics, _build_ctx, self_property, function| {
1279            function.expect_no_arguments()?;
1280            let out_property = self_property.and_then(extract::<String>);
1281            Ok(out_property.into_dyn_wrapped())
1282        },
1283    );
1284    map.insert(
1285        "as_string_list",
1286        |_language, _diagnostics, _build_ctx, self_property, function| {
1287            function.expect_no_arguments()?;
1288            let out_property = self_property.and_then(extract::<Vec<String>>);
1289            Ok(out_property.into_dyn_wrapped())
1290        },
1291    );
1292    // TODO: add is_<type>() -> Boolean?
1293    // TODO: add .get(key) -> ConfigValue or Option<ConfigValue>?
1294    map
1295}
1296
1297fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1298-> TemplateBuildMethodFnMap<'a, L, Signature> {
1299    // Not using maplit::hashmap!{} or custom declarative macro here because
1300    // code completion inside macro is quite restricted.
1301    let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
1302    map.insert(
1303        "name",
1304        |_language, _diagnostics, _build_ctx, self_property, function| {
1305            function.expect_no_arguments()?;
1306            let out_property = self_property.map(|signature| signature.name);
1307            Ok(out_property.into_dyn_wrapped())
1308        },
1309    );
1310    map.insert(
1311        "email",
1312        |_language, _diagnostics, _build_ctx, self_property, function| {
1313            function.expect_no_arguments()?;
1314            let out_property = self_property.map(|signature| Email(signature.email));
1315            Ok(out_property.into_dyn_wrapped())
1316        },
1317    );
1318    map.insert(
1319        "timestamp",
1320        |_language, _diagnostics, _build_ctx, self_property, function| {
1321            function.expect_no_arguments()?;
1322            let out_property = self_property.map(|signature| signature.timestamp);
1323            Ok(out_property.into_dyn_wrapped())
1324        },
1325    );
1326    map
1327}
1328
1329fn builtin_email_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1330-> TemplateBuildMethodFnMap<'a, L, Email> {
1331    // Not using maplit::hashmap!{} or custom declarative macro here because
1332    // code completion inside macro is quite restricted.
1333    let mut map = TemplateBuildMethodFnMap::<L, Email>::new();
1334    map.insert(
1335        "local",
1336        |_language, _diagnostics, _build_ctx, self_property, function| {
1337            function.expect_no_arguments()?;
1338            let out_property = self_property.map(|email| {
1339                let (local, _) = text_util::split_email(&email.0);
1340                local.to_owned()
1341            });
1342            Ok(out_property.into_dyn_wrapped())
1343        },
1344    );
1345    map.insert(
1346        "domain",
1347        |_language, _diagnostics, _build_ctx, self_property, function| {
1348            function.expect_no_arguments()?;
1349            let out_property = self_property.map(|email| {
1350                let (_, domain) = text_util::split_email(&email.0);
1351                domain.unwrap_or_default().to_owned()
1352            });
1353            Ok(out_property.into_dyn_wrapped())
1354        },
1355    );
1356    map
1357}
1358
1359fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1360-> TemplateBuildMethodFnMap<'a, L, SizeHint> {
1361    // Not using maplit::hashmap!{} or custom declarative macro here because
1362    // code completion inside macro is quite restricted.
1363    let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
1364    map.insert(
1365        "lower",
1366        |_language, _diagnostics, _build_ctx, self_property, function| {
1367            function.expect_no_arguments()?;
1368            let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
1369            Ok(out_property.into_dyn_wrapped())
1370        },
1371    );
1372    map.insert(
1373        "upper",
1374        |_language, _diagnostics, _build_ctx, self_property, function| {
1375            function.expect_no_arguments()?;
1376            let out_property =
1377                self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
1378            Ok(out_property.into_dyn_wrapped())
1379        },
1380    );
1381    map.insert(
1382        "exact",
1383        |_language, _diagnostics, _build_ctx, self_property, function| {
1384            function.expect_no_arguments()?;
1385            let out_property = self_property.and_then(|(lower, upper)| {
1386                let exact = (Some(lower) == upper).then_some(lower);
1387                Ok(exact.map(i64::try_from).transpose()?)
1388            });
1389            Ok(out_property.into_dyn_wrapped())
1390        },
1391    );
1392    map.insert(
1393        "zero",
1394        |_language, _diagnostics, _build_ctx, self_property, function| {
1395            function.expect_no_arguments()?;
1396            let out_property = self_property.map(|(_, upper)| upper == Some(0));
1397            Ok(out_property.into_dyn_wrapped())
1398        },
1399    );
1400    map
1401}
1402
1403fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1404-> TemplateBuildMethodFnMap<'a, L, Timestamp> {
1405    // Not using maplit::hashmap!{} or custom declarative macro here because
1406    // code completion inside macro is quite restricted.
1407    let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
1408    map.insert(
1409        "ago",
1410        |_language, _diagnostics, _build_ctx, self_property, function| {
1411            function.expect_no_arguments()?;
1412            let now = Timestamp::now();
1413            let format = timeago::Formatter::new();
1414            let out_property = self_property.and_then(move |timestamp| {
1415                Ok(time_util::format_duration(&timestamp, &now, &format)?)
1416            });
1417            Ok(out_property.into_dyn_wrapped())
1418        },
1419    );
1420    map.insert(
1421        "format",
1422        |_language, diagnostics, _build_ctx, self_property, function| {
1423            // No dynamic string is allowed as the templater has no runtime error type.
1424            let [format_node] = function.expect_exact_arguments()?;
1425            let format =
1426                template_parser::catch_aliases(diagnostics, format_node, |_diagnostics, node| {
1427                    let format = template_parser::expect_string_literal(node)?;
1428                    time_util::FormattingItems::parse(format).ok_or_else(|| {
1429                        TemplateParseError::expression("Invalid time format", node.span)
1430                    })
1431                })?
1432                .into_owned();
1433            let out_property = self_property.and_then(move |timestamp| {
1434                Ok(time_util::format_absolute_timestamp_with(
1435                    &timestamp, &format,
1436                )?)
1437            });
1438            Ok(out_property.into_dyn_wrapped())
1439        },
1440    );
1441    map.insert(
1442        "utc",
1443        |_language, _diagnostics, _build_ctx, self_property, function| {
1444            function.expect_no_arguments()?;
1445            let out_property = self_property.map(|mut timestamp| {
1446                timestamp.tz_offset = 0;
1447                timestamp
1448            });
1449            Ok(out_property.into_dyn_wrapped())
1450        },
1451    );
1452    map.insert(
1453        "local",
1454        |_language, _diagnostics, _build_ctx, self_property, function| {
1455            function.expect_no_arguments()?;
1456            let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
1457                .ok()
1458                .and_then(|tz_string| tz_string.parse::<i32>().ok())
1459                .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
1460            let out_property = self_property.map(move |mut timestamp| {
1461                timestamp.tz_offset = tz_offset;
1462                timestamp
1463            });
1464            Ok(out_property.into_dyn_wrapped())
1465        },
1466    );
1467    map.insert(
1468        "after",
1469        |_language, diagnostics, _build_ctx, self_property, function| {
1470            let [date_pattern_node] = function.expect_exact_arguments()?;
1471            let now = chrono::Local::now();
1472            let date_pattern = template_parser::catch_aliases(
1473                diagnostics,
1474                date_pattern_node,
1475                |_diagnostics, node| {
1476                    let date_pattern = template_parser::expect_string_literal(node)?;
1477                    DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
1478                        TemplateParseError::expression("Invalid date pattern", node.span)
1479                            .with_source(err)
1480                    })
1481                },
1482            )?;
1483            let out_property = self_property.map(move |timestamp| date_pattern.matches(&timestamp));
1484            Ok(out_property.into_dyn_wrapped())
1485        },
1486    );
1487    map.insert("before", map["after"]);
1488    map.insert(
1489        "since",
1490        |language, diagnostics, build_ctx, self_property, function| {
1491            let [date_node] = function.expect_exact_arguments()?;
1492            let date_property =
1493                expect_timestamp_expression(language, diagnostics, build_ctx, date_node)?;
1494            let out_property =
1495                (self_property, date_property).and_then(move |(self_timestamp, arg_timestamp)| {
1496                    Ok(TimestampRange {
1497                        start: arg_timestamp,
1498                        end: self_timestamp,
1499                    })
1500                });
1501            Ok(out_property.into_dyn_wrapped())
1502        },
1503    );
1504    map
1505}
1506
1507fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1508-> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
1509    // Not using maplit::hashmap!{} or custom declarative macro here because
1510    // code completion inside macro is quite restricted.
1511    let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
1512    map.insert(
1513        "start",
1514        |_language, _diagnostics, _build_ctx, self_property, function| {
1515            function.expect_no_arguments()?;
1516            let out_property = self_property.map(|time_range| time_range.start);
1517            Ok(out_property.into_dyn_wrapped())
1518        },
1519    );
1520    map.insert(
1521        "end",
1522        |_language, _diagnostics, _build_ctx, self_property, function| {
1523            function.expect_no_arguments()?;
1524            let out_property = self_property.map(|time_range| time_range.end);
1525            Ok(out_property.into_dyn_wrapped())
1526        },
1527    );
1528    map.insert(
1529        "duration",
1530        |_language, _diagnostics, _build_ctx, self_property, function| {
1531            function.expect_no_arguments()?;
1532            // TODO: Introduce duration type, and move formatting to it.
1533            let out_property = self_property.and_then(|time_range| {
1534                let mut f = timeago::Formatter::new();
1535                f.min_unit(timeago::TimeUnit::Microseconds).ago("");
1536                let duration = time_util::format_duration(&time_range.start, &time_range.end, &f)?;
1537                if duration == "now" {
1538                    Ok("less than a microsecond".to_owned())
1539                } else {
1540                    Ok(duration)
1541                }
1542            });
1543            Ok(out_property.into_dyn_wrapped())
1544        },
1545    );
1546    map
1547}
1548
1549fn builtin_any_list_methods<'a, L: TemplateLanguage<'a> + ?Sized>() -> BuildAnyMethodFnMap<'a, L> {
1550    // Not using maplit::hashmap!{} or custom declarative macro here because
1551    // code completion inside macro is quite restricted.
1552    let mut map = BuildAnyMethodFnMap::<L>::new();
1553    map.insert(
1554        "join",
1555        |language, diagnostics, build_ctx, self_template, function| {
1556            let [separator_node] = function.expect_exact_arguments()?;
1557            let separator =
1558                expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1559            Ok(L::Property::wrap_template(
1560                self_template.try_join(separator).ok_or_else(|| {
1561                    // FIXME: This error should probably be reported on the type
1562                    // within the AnyListTemplateProperty.
1563                    TemplateParseError::expected_type("Template", "AnyList", function.name_span)
1564                })?,
1565            ))
1566        },
1567    );
1568    map
1569}
1570
1571/// Creates new symbol table for printable list property.
1572pub fn builtin_formattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1573where
1574    L: TemplateLanguage<'a> + ?Sized,
1575    L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1576    O: Template + Clone + 'a,
1577{
1578    let mut map = builtin_unformattable_list_methods::<L, O>();
1579    map.insert(
1580        "join",
1581        |language, diagnostics, build_ctx, self_property, function| {
1582            let [separator_node] = function.expect_exact_arguments()?;
1583            let separator =
1584                expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1585            let template =
1586                ListPropertyTemplate::new(self_property, separator, |formatter, item| {
1587                    item.format(formatter)
1588                });
1589            Ok(L::Property::wrap_template(Box::new(template)))
1590        },
1591    );
1592    map
1593}
1594
1595/// Creates new symbol table for unprintable list property.
1596pub fn builtin_unformattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1597where
1598    L: TemplateLanguage<'a> + ?Sized,
1599    L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1600    O: Clone + 'a,
1601{
1602    // Not using maplit::hashmap!{} or custom declarative macro here because
1603    // code completion inside macro is quite restricted.
1604    let mut map = TemplateBuildMethodFnMap::<L, Vec<O>>::new();
1605    map.insert(
1606        "len",
1607        |_language, _diagnostics, _build_ctx, self_property, function| {
1608            function.expect_no_arguments()?;
1609            let out_property = self_property.and_then(|items| Ok(i64::try_from(items.len())?));
1610            Ok(out_property.into_dyn_wrapped())
1611        },
1612    );
1613    map.insert(
1614        "filter",
1615        |language, diagnostics, build_ctx, self_property, function| {
1616            let out_property: BoxedTemplateProperty<'a, Vec<O>> =
1617                build_filter_operation(language, diagnostics, build_ctx, self_property, function)?;
1618            Ok(L::Property::wrap_property(out_property))
1619        },
1620    );
1621    map.insert(
1622        "map",
1623        |language, diagnostics, build_ctx, self_property, function| {
1624            let map_result =
1625                build_map_operation(language, diagnostics, build_ctx, self_property, function)?;
1626            Ok(L::Property::wrap_any_list(map_result))
1627        },
1628    );
1629    map.insert(
1630        "any",
1631        |language, diagnostics, build_ctx, self_property, function| {
1632            let out_property =
1633                build_any_operation(language, diagnostics, build_ctx, self_property, function)?;
1634            Ok(out_property.into_dyn_wrapped())
1635        },
1636    );
1637    map.insert(
1638        "all",
1639        |language, diagnostics, build_ctx, self_property, function| {
1640            let out_property =
1641                build_all_operation(language, diagnostics, build_ctx, self_property, function)?;
1642            Ok(out_property.into_dyn_wrapped())
1643        },
1644    );
1645    map.insert(
1646        "first",
1647        |_language, _diagnostics, _build_ctx, self_property, function| {
1648            function.expect_no_arguments()?;
1649            // TODO: Return `Option<T>` instead of erroring out.
1650            let out_property = self_property.and_then(|items| {
1651                items
1652                    .into_iter()
1653                    .next()
1654                    .ok_or_else(|| TemplatePropertyError("List is empty".into()))
1655            });
1656            Ok(L::Property::wrap_property(out_property.into_dyn()))
1657        },
1658    );
1659    map.insert(
1660        "last",
1661        |_language, _diagnostics, _build_ctx, self_property, function| {
1662            function.expect_no_arguments()?;
1663            // TODO: Return `Option<T>` instead of erroring out.
1664            let out_property = self_property.and_then(|mut items| {
1665                items
1666                    .pop()
1667                    .ok_or_else(|| TemplatePropertyError("List is empty".into()))
1668            });
1669            Ok(L::Property::wrap_property(out_property.into_dyn()))
1670        },
1671    );
1672    map.insert(
1673        "get",
1674        |language, diagnostics, build_ctx, self_property, function| {
1675            let [index_node] = function.expect_exact_arguments()?;
1676            let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
1677            // TODO: Return `Option<T>` instead of erroring out.
1678            let out_property = (self_property, index).and_then(|(mut items, index)| {
1679                if index < items.len() {
1680                    Ok(items.remove(index))
1681                } else {
1682                    Err(TemplatePropertyError(
1683                        format!("Index {index} out of bounds").into(),
1684                    ))
1685                }
1686            });
1687            Ok(L::Property::wrap_property(out_property.into_dyn()))
1688        },
1689    );
1690    map.insert(
1691        "reverse",
1692        |_language, _diagnostics, _build_ctx, self_property, function| {
1693            function.expect_no_arguments()?;
1694            let out_property = self_property.map(|mut items| {
1695                items.reverse();
1696                items
1697            });
1698            Ok(L::Property::wrap_property(out_property.into_dyn()))
1699        },
1700    );
1701    map.insert(
1702        "skip",
1703        |language, diagnostics, build_ctx, self_property, function| {
1704            let [count_node] = function.expect_exact_arguments()?;
1705            let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
1706            let out_property = (self_property, count)
1707                .map(|(items, count)| items.into_iter().skip(count).collect_vec());
1708            Ok(L::Property::wrap_property(out_property.into_dyn()))
1709        },
1710    );
1711    map.insert(
1712        "take",
1713        |language, diagnostics, build_ctx, self_property, function| {
1714            let [count_node] = function.expect_exact_arguments()?;
1715            let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
1716            let out_property = (self_property, count)
1717                .map(|(items, count)| items.into_iter().take(count).collect_vec());
1718            Ok(L::Property::wrap_property(out_property.into_dyn()))
1719        },
1720    );
1721    map
1722}
1723
1724/// Builds expression that extracts iterable property and filters its items.
1725fn build_filter_operation<'a, L, O, P, B>(
1726    language: &L,
1727    diagnostics: &mut TemplateDiagnostics,
1728    build_ctx: &BuildContext<L::Property>,
1729    self_property: P,
1730    function: &FunctionCallNode,
1731) -> TemplateParseResult<BoxedTemplateProperty<'a, B>>
1732where
1733    L: TemplateLanguage<'a> + ?Sized,
1734    L::Property: WrapTemplateProperty<'a, O>,
1735    P: TemplateProperty + 'a,
1736    P::Output: IntoIterator<Item = O>,
1737    O: Clone + 'a,
1738    B: FromIterator<O>,
1739{
1740    let [lambda_node] = function.expect_exact_arguments()?;
1741    let item_placeholder = PropertyPlaceholder::new();
1742    let item_predicate =
1743        template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1744            let lambda = template_parser::expect_lambda(node)?;
1745            build_lambda_expression(
1746                build_ctx,
1747                lambda,
1748                &[&|| item_placeholder.clone().into_dyn_wrapped()],
1749                |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1750            )
1751        })?;
1752    let out_property = self_property.and_then(move |items| {
1753        items
1754            .into_iter()
1755            .filter_map(|item| {
1756                // Evaluate predicate with the current item
1757                item_placeholder.set(item);
1758                let result = item_predicate.extract();
1759                let item = item_placeholder.take().unwrap();
1760                result.map(|pred| pred.then_some(item)).transpose()
1761            })
1762            .collect()
1763    });
1764    Ok(out_property.into_dyn())
1765}
1766
1767/// Builds expression that extracts iterable property and applies template to
1768/// each item.
1769fn build_map_operation<'a, L, O, P>(
1770    language: &L,
1771    diagnostics: &mut TemplateDiagnostics,
1772    build_ctx: &BuildContext<L::Property>,
1773    self_property: P,
1774    function: &FunctionCallNode,
1775) -> TemplateParseResult<BoxedAnyProperty<'a>>
1776where
1777    L: TemplateLanguage<'a> + ?Sized,
1778    L::Property: WrapTemplateProperty<'a, O>,
1779    P: TemplateProperty + 'a,
1780    P::Output: IntoIterator<Item = O>,
1781    O: Clone + 'a,
1782{
1783    let [lambda_node] = function.expect_exact_arguments()?;
1784    let item_placeholder = PropertyPlaceholder::new();
1785    let mapped_item =
1786        template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1787            let lambda = template_parser::expect_lambda(node)?;
1788            build_lambda_expression(
1789                build_ctx,
1790                lambda,
1791                &[&|| item_placeholder.clone().into_dyn_wrapped()],
1792                |build_ctx, body| expect_any_expression(language, diagnostics, build_ctx, body),
1793            )
1794        })?;
1795    let mapped_list = ListMapProperty::new(self_property, item_placeholder, mapped_item);
1796    Ok(Box::new(mapped_list))
1797}
1798
1799/// Builds expression that checks if any item in the list satisfies the
1800/// predicate.
1801fn build_any_operation<'a, L, O, P>(
1802    language: &L,
1803    diagnostics: &mut TemplateDiagnostics,
1804    build_ctx: &BuildContext<L::Property>,
1805    self_property: P,
1806    function: &FunctionCallNode,
1807) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
1808where
1809    L: TemplateLanguage<'a> + ?Sized,
1810    L::Property: WrapTemplateProperty<'a, O>,
1811    P: TemplateProperty + 'a,
1812    P::Output: IntoIterator<Item = O>,
1813    O: Clone + 'a,
1814{
1815    let [lambda_node] = function.expect_exact_arguments()?;
1816    let item_placeholder = PropertyPlaceholder::new();
1817    let item_predicate =
1818        template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1819            let lambda = template_parser::expect_lambda(node)?;
1820            build_lambda_expression(
1821                build_ctx,
1822                lambda,
1823                &[&|| item_placeholder.clone().into_dyn_wrapped()],
1824                |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1825            )
1826        })?;
1827
1828    let out_property = self_property.and_then(move |items| {
1829        items
1830            .into_iter()
1831            .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
1832            .process_results(|mut predicates| predicates.any(|p| p))
1833    });
1834    Ok(out_property.into_dyn())
1835}
1836
1837/// Builds expression that checks if all items in the list satisfy the
1838/// predicate.
1839fn build_all_operation<'a, L, O, P>(
1840    language: &L,
1841    diagnostics: &mut TemplateDiagnostics,
1842    build_ctx: &BuildContext<L::Property>,
1843    self_property: P,
1844    function: &FunctionCallNode,
1845) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
1846where
1847    L: TemplateLanguage<'a> + ?Sized,
1848    L::Property: WrapTemplateProperty<'a, O>,
1849    P: TemplateProperty + 'a,
1850    P::Output: IntoIterator<Item = O>,
1851    O: Clone + 'a,
1852{
1853    let [lambda_node] = function.expect_exact_arguments()?;
1854    let item_placeholder = PropertyPlaceholder::new();
1855    let item_predicate =
1856        template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1857            let lambda = template_parser::expect_lambda(node)?;
1858            build_lambda_expression(
1859                build_ctx,
1860                lambda,
1861                &[&|| item_placeholder.clone().into_dyn_wrapped()],
1862                |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1863            )
1864        })?;
1865
1866    let out_property = self_property.and_then(move |items| {
1867        items
1868            .into_iter()
1869            .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
1870            .process_results(|mut predicates| predicates.all(|p| p))
1871    });
1872    Ok(out_property.into_dyn())
1873}
1874
1875/// Builds lambda expression to be evaluated with the provided arguments.
1876/// `arg_fns` is usually an array of wrapped [`PropertyPlaceholder`]s.
1877fn build_lambda_expression<'i, P, T>(
1878    build_ctx: &BuildContext<'i, P>,
1879    lambda: &LambdaNode<'i>,
1880    arg_fns: &[&'i dyn Fn() -> P],
1881    build_body: impl FnOnce(&BuildContext<'i, P>, &ExpressionNode<'i>) -> TemplateParseResult<T>,
1882) -> TemplateParseResult<T> {
1883    if lambda.params.len() != arg_fns.len() {
1884        return Err(TemplateParseError::expression(
1885            format!("Expected {} lambda parameters", arg_fns.len()),
1886            lambda.params_span,
1887        ));
1888    }
1889    let mut local_variables = build_ctx.local_variables.clone();
1890    local_variables.extend(iter::zip(&lambda.params, arg_fns));
1891    let inner_build_ctx = BuildContext {
1892        local_variables,
1893        self_variable: build_ctx.self_variable,
1894    };
1895    build_body(&inner_build_ctx, &lambda.body)
1896}
1897
1898fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
1899    // Not using maplit::hashmap!{} or custom declarative macro here because
1900    // code completion inside macro is quite restricted.
1901    let mut map = TemplateBuildFunctionFnMap::<L>::new();
1902    map.insert("fill", |language, diagnostics, build_ctx, function| {
1903        let [width_node, content_node] = function.expect_exact_arguments()?;
1904        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1905        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1906        let template =
1907            ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
1908                Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
1909                Err(err) => formatter.handle_error(err),
1910            });
1911        Ok(L::Property::wrap_template(Box::new(template)))
1912    });
1913    map.insert("indent", |language, diagnostics, build_ctx, function| {
1914        let [prefix_node, content_node] = function.expect_exact_arguments()?;
1915        let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1916        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1917        let template = ReformatTemplate::new(content, move |formatter, recorded| {
1918            let rewrap = formatter.rewrap_fn();
1919            text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
1920                prefix.format(&mut rewrap(formatter))
1921            })
1922        });
1923        Ok(L::Property::wrap_template(Box::new(template)))
1924    });
1925    map.insert("pad_start", |language, diagnostics, build_ctx, function| {
1926        let ([width_node, content_node], [fill_char_node]) =
1927            function.expect_named_arguments(&["", "", "fill_char"])?;
1928        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1929        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1930        let fill_char = fill_char_node
1931            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1932            .transpose()?;
1933        let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
1934        Ok(L::Property::wrap_template(template))
1935    });
1936    map.insert("pad_end", |language, diagnostics, build_ctx, function| {
1937        let ([width_node, content_node], [fill_char_node]) =
1938            function.expect_named_arguments(&["", "", "fill_char"])?;
1939        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1940        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1941        let fill_char = fill_char_node
1942            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1943            .transpose()?;
1944        let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
1945        Ok(L::Property::wrap_template(template))
1946    });
1947    map.insert(
1948        "pad_centered",
1949        |language, diagnostics, build_ctx, function| {
1950            let ([width_node, content_node], [fill_char_node]) =
1951                function.expect_named_arguments(&["", "", "fill_char"])?;
1952            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1953            let content =
1954                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1955            let fill_char = fill_char_node
1956                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1957                .transpose()?;
1958            let template =
1959                new_pad_template(content, fill_char, width, text_util::write_padded_centered);
1960            Ok(L::Property::wrap_template(template))
1961        },
1962    );
1963    map.insert(
1964        "truncate_start",
1965        |language, diagnostics, build_ctx, function| {
1966            let ([width_node, content_node], [ellipsis_node]) =
1967                function.expect_named_arguments(&["", "", "ellipsis"])?;
1968            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1969            let content =
1970                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1971            let ellipsis = ellipsis_node
1972                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1973                .transpose()?;
1974            let template =
1975                new_truncate_template(content, ellipsis, width, text_util::write_truncated_start);
1976            Ok(L::Property::wrap_template(template))
1977        },
1978    );
1979    map.insert(
1980        "truncate_end",
1981        |language, diagnostics, build_ctx, function| {
1982            let ([width_node, content_node], [ellipsis_node]) =
1983                function.expect_named_arguments(&["", "", "ellipsis"])?;
1984            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1985            let content =
1986                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1987            let ellipsis = ellipsis_node
1988                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1989                .transpose()?;
1990            let template =
1991                new_truncate_template(content, ellipsis, width, text_util::write_truncated_end);
1992            Ok(L::Property::wrap_template(template))
1993        },
1994    );
1995    map.insert("hash", |language, diagnostics, build_ctx, function| {
1996        let [content_node] = function.expect_exact_arguments()?;
1997        let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
1998        let result = content.map(|c| hex_util::encode_hex(blake2b_hash(&c).as_ref()));
1999        Ok(result.into_dyn_wrapped())
2000    });
2001    map.insert("label", |language, diagnostics, build_ctx, function| {
2002        let [label_node, content_node] = function.expect_exact_arguments()?;
2003        let label_property =
2004            expect_stringify_expression(language, diagnostics, build_ctx, label_node)?;
2005        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2006        let labels =
2007            label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
2008        Ok(L::Property::wrap_template(Box::new(LabelTemplate::new(
2009            content, labels,
2010        ))))
2011    });
2012    map.insert(
2013        "raw_escape_sequence",
2014        |language, diagnostics, build_ctx, function| {
2015            let [content_node] = function.expect_exact_arguments()?;
2016            let content =
2017                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2018            Ok(L::Property::wrap_template(Box::new(
2019                RawEscapeSequenceTemplate(content),
2020            )))
2021        },
2022    );
2023    map.insert("hyperlink", |language, diagnostics, build_ctx, function| {
2024        let ([url_node, text_node], [fallback_node]) = function.expect_arguments()?;
2025        let url = expect_stringify_expression(language, diagnostics, build_ctx, url_node)?;
2026        let text = expect_template_expression(language, diagnostics, build_ctx, text_node)?;
2027        let fallback = fallback_node
2028            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2029            .transpose()?;
2030        Ok(L::Property::wrap_template(Box::new(
2031            HyperlinkTemplate::new(url, text, fallback),
2032        )))
2033    });
2034    map.insert("stringify", |language, diagnostics, build_ctx, function| {
2035        let [content_node] = function.expect_exact_arguments()?;
2036        let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
2037        Ok(L::Property::wrap_property(content))
2038    });
2039    map.insert("json", |language, diagnostics, build_ctx, function| {
2040        // TODO: Add pretty=true|false? or json(key=value, ..)? The latter might
2041        // be implemented as a map constructor/literal if we add support for
2042        // heterogeneous list/map types.
2043        let [value_node] = function.expect_exact_arguments()?;
2044        let value = expect_serialize_expression(language, diagnostics, build_ctx, value_node)?;
2045        let out_property = value.and_then(|v| Ok(serde_json::to_string(&v)?));
2046        Ok(out_property.into_dyn_wrapped())
2047    });
2048    map.insert("if", |language, diagnostics, build_ctx, function| {
2049        let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
2050        let condition =
2051            expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
2052        let true_any = expect_any_expression(language, diagnostics, build_ctx, true_node)?;
2053        let false_any = false_node
2054            .map(|node| expect_any_expression(language, diagnostics, build_ctx, node))
2055            .transpose()?;
2056        let property = ConditionalProperty::new(condition, true_any, false_any);
2057        Ok(L::Property::wrap_any(Box::new(property)))
2058    });
2059    map.insert("coalesce", |language, diagnostics, build_ctx, function| {
2060        let ([], content_nodes) = function.expect_some_arguments()?;
2061        let contents = content_nodes
2062            .iter()
2063            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2064            .try_collect()?;
2065        Ok(L::Property::wrap_template(Box::new(CoalesceTemplate(
2066            contents,
2067        ))))
2068    });
2069    map.insert("concat", |language, diagnostics, build_ctx, function| {
2070        let ([], content_nodes) = function.expect_some_arguments()?;
2071        let contents = content_nodes
2072            .iter()
2073            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2074            .try_collect()?;
2075        Ok(L::Property::wrap_template(Box::new(ConcatTemplate(
2076            contents,
2077        ))))
2078    });
2079    map.insert("join", |language, diagnostics, build_ctx, function| {
2080        let ([separator_node], content_nodes) = function.expect_some_arguments()?;
2081        let separator =
2082            expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2083        let contents = content_nodes
2084            .iter()
2085            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2086            .try_collect()?;
2087        Ok(L::Property::wrap_template(Box::new(JoinTemplate::new(
2088            separator, contents,
2089        ))))
2090    });
2091    map.insert("separate", |language, diagnostics, build_ctx, function| {
2092        let ([separator_node], content_nodes) = function.expect_some_arguments()?;
2093        let separator =
2094            expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2095        let contents = content_nodes
2096            .iter()
2097            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2098            .try_collect()?;
2099        Ok(L::Property::wrap_template(Box::new(SeparateTemplate::new(
2100            separator, contents,
2101        ))))
2102    });
2103    map.insert("surround", |language, diagnostics, build_ctx, function| {
2104        let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
2105        let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
2106        let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
2107        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2108        let template = ReformatTemplate::new(content, move |formatter, recorded| {
2109            if recorded.data().is_empty() {
2110                return Ok(());
2111            }
2112            prefix.format(formatter)?;
2113            recorded.replay(formatter.as_mut())?;
2114            suffix.format(formatter)?;
2115            Ok(())
2116        });
2117        Ok(L::Property::wrap_template(Box::new(template)))
2118    });
2119    map.insert("config", |language, diagnostics, build_ctx, function| {
2120        let [name_node] = function.expect_exact_arguments()?;
2121        let name_expression =
2122            expect_stringify_expression(language, diagnostics, build_ctx, name_node)?;
2123        if let Ok(name) = name_expression.extract() {
2124            let config_path: ConfigNamePathBuf = name.parse().map_err(|err| {
2125                TemplateParseError::expression("Failed to parse config name", name_node.span)
2126                    .with_source(err)
2127            })?;
2128            let value = language
2129                .settings()
2130                .get_value(config_path)
2131                .optional()
2132                .map_err(|err| {
2133                    TemplateParseError::expression("Failed to get config value", function.name_span)
2134                        .with_source(err)
2135                })?;
2136            // .decorated("", "") to trim leading/trailing whitespace
2137            Ok(Literal(value.map(|v| v.decorated("", ""))).into_dyn_wrapped())
2138        } else {
2139            let settings = language.settings().clone();
2140            let out_property = name_expression.and_then(move |name| {
2141                let config_path: ConfigNamePathBuf = name.parse()?;
2142                let value = settings.get_value(config_path).optional()?;
2143                Ok(value.map(|v| v.decorated("", "")))
2144            });
2145            Ok(out_property.into_dyn_wrapped())
2146        }
2147    });
2148    map
2149}
2150
2151fn new_pad_template<'a, W>(
2152    content: Box<dyn Template + 'a>,
2153    fill_char: Option<Box<dyn Template + 'a>>,
2154    width: BoxedTemplateProperty<'a, usize>,
2155    write_padded: W,
2156) -> Box<dyn Template + 'a>
2157where
2158    W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
2159{
2160    let default_fill_char = FormatRecorder::with_data(" ");
2161    let template = ReformatTemplate::new(content, move |formatter, recorded| {
2162        let width = match width.extract() {
2163            Ok(width) => width,
2164            Err(err) => return formatter.handle_error(err),
2165        };
2166        let mut fill_char_recorder;
2167        let recorded_fill_char = if let Some(fill_char) = &fill_char {
2168            let rewrap = formatter.rewrap_fn();
2169            fill_char_recorder = FormatRecorder::new(formatter.maybe_color());
2170            fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
2171            &fill_char_recorder
2172        } else {
2173            &default_fill_char
2174        };
2175        write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
2176    });
2177    Box::new(template)
2178}
2179
2180fn new_truncate_template<'a, W>(
2181    content: Box<dyn Template + 'a>,
2182    ellipsis: Option<Box<dyn Template + 'a>>,
2183    width: BoxedTemplateProperty<'a, usize>,
2184    write_truncated: W,
2185) -> Box<dyn Template + 'a>
2186where
2187    W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<usize> + 'a,
2188{
2189    let default_ellipsis = FormatRecorder::with_data("");
2190    let template = ReformatTemplate::new(content, move |formatter, recorded| {
2191        let width = match width.extract() {
2192            Ok(width) => width,
2193            Err(err) => return formatter.handle_error(err),
2194        };
2195        let mut ellipsis_recorder;
2196        let recorded_ellipsis = if let Some(ellipsis) = &ellipsis {
2197            let rewrap = formatter.rewrap_fn();
2198            ellipsis_recorder = FormatRecorder::new(formatter.maybe_color());
2199            ellipsis.format(&mut rewrap(&mut ellipsis_recorder))?;
2200            &ellipsis_recorder
2201        } else {
2202            &default_ellipsis
2203        };
2204        write_truncated(formatter.as_mut(), recorded, recorded_ellipsis, width)?;
2205        Ok(())
2206    });
2207    Box::new(template)
2208}
2209
2210/// Builds intermediate expression tree from AST nodes.
2211pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2212    language: &L,
2213    diagnostics: &mut TemplateDiagnostics,
2214    build_ctx: &BuildContext<L::Property>,
2215    node: &ExpressionNode,
2216) -> TemplateParseResult<Expression<L::Property>> {
2217    template_parser::catch_aliases(diagnostics, node, |diagnostics, node| match &node.kind {
2218        ExpressionKind::Identifier(name) => {
2219            if let Some(make) = build_ctx.local_variables.get(name) {
2220                // Don't label a local variable with its name
2221                Ok(Expression::unlabeled(make()))
2222            } else if *name == "self" {
2223                // "self" is a special variable, so don't label it
2224                let make = build_ctx.self_variable;
2225                Ok(Expression::unlabeled(make()))
2226            } else {
2227                let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
2228                    .map_err(|err| {
2229                        err.extend_keyword_candidates(itertools::chain(
2230                            build_ctx.local_variables.keys().copied(),
2231                            ["self"],
2232                        ))
2233                    })?;
2234                Ok(Expression::with_label(property, *name))
2235            }
2236        }
2237        ExpressionKind::Boolean(value) => {
2238            let property = Literal(*value).into_dyn_wrapped();
2239            Ok(Expression::unlabeled(property))
2240        }
2241        ExpressionKind::Integer(value) => {
2242            let property = Literal(*value).into_dyn_wrapped();
2243            Ok(Expression::unlabeled(property))
2244        }
2245        ExpressionKind::String(value) => {
2246            let property = Literal(value.clone()).into_dyn_wrapped();
2247            Ok(Expression::unlabeled(property))
2248        }
2249        ExpressionKind::Pattern(_) => Err(TemplateParseError::expression(
2250            "String patterns may not be used as expression values",
2251            node.span,
2252        )),
2253        ExpressionKind::Unary(op, arg_node) => {
2254            let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
2255            Ok(Expression::unlabeled(property))
2256        }
2257        ExpressionKind::Binary(op, lhs_node, rhs_node) => {
2258            let property = build_binary_operation(
2259                language,
2260                diagnostics,
2261                build_ctx,
2262                *op,
2263                lhs_node,
2264                rhs_node,
2265                node.span,
2266            )?;
2267            Ok(Expression::unlabeled(property))
2268        }
2269        ExpressionKind::Concat(nodes) => {
2270            let templates = nodes
2271                .iter()
2272                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2273                .try_collect()?;
2274            let property = L::Property::wrap_template(Box::new(ConcatTemplate(templates)));
2275            Ok(Expression::unlabeled(property))
2276        }
2277        ExpressionKind::FunctionCall(function) => {
2278            let property = language.build_function(diagnostics, build_ctx, function)?;
2279            Ok(Expression::unlabeled(property))
2280        }
2281        ExpressionKind::MethodCall(method) => {
2282            let mut expression =
2283                build_expression(language, diagnostics, build_ctx, &method.object)?;
2284            expression.property = language.build_method(
2285                diagnostics,
2286                build_ctx,
2287                expression.property,
2288                &method.function,
2289            )?;
2290            expression.labels.push(method.function.name.to_owned());
2291            Ok(expression)
2292        }
2293        ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
2294            "Lambda cannot be defined here",
2295            node.span,
2296        )),
2297        ExpressionKind::AliasExpanded(..) => unreachable!(),
2298    })
2299}
2300
2301/// Builds template evaluation tree from AST nodes, with fresh build context.
2302pub fn build<'a, C, L>(
2303    language: &L,
2304    diagnostics: &mut TemplateDiagnostics,
2305    node: &ExpressionNode,
2306) -> TemplateParseResult<TemplateRenderer<'a, C>>
2307where
2308    C: Clone + 'a,
2309    L: TemplateLanguage<'a> + ?Sized,
2310    L::Property: WrapTemplateProperty<'a, C>,
2311{
2312    let self_placeholder = PropertyPlaceholder::new();
2313    let build_ctx = BuildContext {
2314        local_variables: HashMap::new(),
2315        self_variable: &|| self_placeholder.clone().into_dyn_wrapped(),
2316    };
2317    let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
2318    Ok(TemplateRenderer::new(template, self_placeholder))
2319}
2320
2321/// Parses text, expands aliases, then builds template evaluation tree.
2322pub fn parse<'a, C, L>(
2323    language: &L,
2324    diagnostics: &mut TemplateDiagnostics,
2325    template_text: &str,
2326    aliases_map: &TemplateAliasesMap,
2327) -> TemplateParseResult<TemplateRenderer<'a, C>>
2328where
2329    C: Clone + 'a,
2330    L: TemplateLanguage<'a> + ?Sized,
2331    L::Property: WrapTemplateProperty<'a, C>,
2332{
2333    let node = template_parser::parse(template_text, aliases_map)?;
2334    build(language, diagnostics, &node).map_err(|err| err.extend_alias_candidates(aliases_map))
2335}
2336
2337pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2338    language: &L,
2339    diagnostics: &mut TemplateDiagnostics,
2340    build_ctx: &BuildContext<L::Property>,
2341    node: &ExpressionNode,
2342) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>> {
2343    expect_expression_of_type(
2344        language,
2345        diagnostics,
2346        build_ctx,
2347        node,
2348        "Boolean",
2349        |expression| expression.try_into_boolean(),
2350    )
2351}
2352
2353pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2354    language: &L,
2355    diagnostics: &mut TemplateDiagnostics,
2356    build_ctx: &BuildContext<L::Property>,
2357    node: &ExpressionNode,
2358) -> TemplateParseResult<BoxedTemplateProperty<'a, i64>> {
2359    expect_expression_of_type(
2360        language,
2361        diagnostics,
2362        build_ctx,
2363        node,
2364        "Integer",
2365        |expression| expression.try_into_integer(),
2366    )
2367}
2368
2369/// If the given expression `node` is of `Integer` type, converts it to `isize`.
2370pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2371    language: &L,
2372    diagnostics: &mut TemplateDiagnostics,
2373    build_ctx: &BuildContext<L::Property>,
2374    node: &ExpressionNode,
2375) -> TemplateParseResult<BoxedTemplateProperty<'a, isize>> {
2376    let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2377    let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
2378    Ok(isize_property.into_dyn())
2379}
2380
2381/// If the given expression `node` is of `Integer` type, converts it to `usize`.
2382pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2383    language: &L,
2384    diagnostics: &mut TemplateDiagnostics,
2385    build_ctx: &BuildContext<L::Property>,
2386    node: &ExpressionNode,
2387) -> TemplateParseResult<BoxedTemplateProperty<'a, usize>> {
2388    let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2389    let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
2390    Ok(usize_property.into_dyn())
2391}
2392
2393pub fn expect_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2394    language: &L,
2395    diagnostics: &mut TemplateDiagnostics,
2396    build_ctx: &BuildContext<L::Property>,
2397    node: &ExpressionNode,
2398) -> TemplateParseResult<BoxedTemplateProperty<'a, String>> {
2399    // Since any formattable type can be converted to a string property, the
2400    // expected type is not a String.
2401    expect_expression_of_type(
2402        language,
2403        diagnostics,
2404        build_ctx,
2405        node,
2406        "Stringify",
2407        |expression| expression.try_into_stringify(),
2408    )
2409}
2410
2411pub fn expect_timestamp_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2412    language: &L,
2413    diagnostics: &mut TemplateDiagnostics,
2414    build_ctx: &BuildContext<L::Property>,
2415    node: &ExpressionNode,
2416) -> TemplateParseResult<BoxedTemplateProperty<'a, Timestamp>> {
2417    expect_expression_of_type(
2418        language,
2419        diagnostics,
2420        build_ctx,
2421        node,
2422        "Timestamp",
2423        |expression| expression.try_into_timestamp(),
2424    )
2425}
2426
2427pub fn expect_serialize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2428    language: &L,
2429    diagnostics: &mut TemplateDiagnostics,
2430    build_ctx: &BuildContext<L::Property>,
2431    node: &ExpressionNode,
2432) -> TemplateParseResult<BoxedSerializeProperty<'a>> {
2433    expect_expression_of_type(
2434        language,
2435        diagnostics,
2436        build_ctx,
2437        node,
2438        "Serialize",
2439        |expression| expression.try_into_serialize(),
2440    )
2441}
2442
2443pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2444    language: &L,
2445    diagnostics: &mut TemplateDiagnostics,
2446    build_ctx: &BuildContext<L::Property>,
2447    node: &ExpressionNode,
2448) -> TemplateParseResult<Box<dyn Template + 'a>> {
2449    expect_expression_of_type(
2450        language,
2451        diagnostics,
2452        build_ctx,
2453        node,
2454        "Template",
2455        |expression| expression.try_into_template(),
2456    )
2457}
2458
2459pub fn expect_any_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2460    language: &L,
2461    diagnostics: &mut TemplateDiagnostics,
2462    build_ctx: &BuildContext<L::Property>,
2463    node: &ExpressionNode,
2464) -> TemplateParseResult<BoxedAnyProperty<'a>> {
2465    template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
2466        Ok(
2467            Box::new(build_expression(language, diagnostics, build_ctx, node)?)
2468                as BoxedAnyProperty<'a>,
2469        )
2470    })
2471}
2472
2473fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
2474    language: &L,
2475    diagnostics: &mut TemplateDiagnostics,
2476    build_ctx: &BuildContext<L::Property>,
2477    node: &ExpressionNode,
2478    expected_type: &str,
2479    f: impl FnOnce(Expression<L::Property>) -> Option<T>,
2480) -> TemplateParseResult<T> {
2481    template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
2482        let expression = build_expression(language, diagnostics, build_ctx, node)?;
2483        let actual_type = expression.type_name();
2484        f(expression)
2485            .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
2486    })
2487}
2488
2489#[cfg(test)]
2490mod tests {
2491    use assert_matches::assert_matches;
2492    use jj_lib::backend::MillisSinceEpoch;
2493    use jj_lib::config::StackedConfig;
2494
2495    use super::*;
2496    use crate::formatter;
2497    use crate::formatter::ColorFormatter;
2498    use crate::generic_templater;
2499    use crate::generic_templater::GenericTemplateLanguage;
2500
2501    #[derive(Clone, Debug, serde::Serialize)]
2502    struct Context;
2503
2504    type TestTemplateLanguage = GenericTemplateLanguage<'static, Context>;
2505    type TestTemplatePropertyKind = <TestTemplateLanguage as TemplateLanguage<'static>>::Property;
2506
2507    generic_templater::impl_self_property_wrapper!(Context);
2508
2509    /// Helper to set up template evaluation environment.
2510    struct TestTemplateEnv {
2511        language: TestTemplateLanguage,
2512        aliases_map: TemplateAliasesMap,
2513        color_rules: Vec<(Vec<String>, formatter::Style)>,
2514    }
2515
2516    impl TestTemplateEnv {
2517        fn new() -> Self {
2518            Self::with_config(StackedConfig::with_defaults())
2519        }
2520
2521        fn with_config(config: StackedConfig) -> Self {
2522            let settings = UserSettings::from_config(config).unwrap();
2523            Self {
2524                language: TestTemplateLanguage::new(&settings),
2525                aliases_map: TemplateAliasesMap::new(),
2526                color_rules: Vec::new(),
2527            }
2528        }
2529    }
2530
2531    impl TestTemplateEnv {
2532        fn add_keyword<F>(&mut self, name: &'static str, build: F)
2533        where
2534            F: Fn() -> TestTemplatePropertyKind + 'static,
2535        {
2536            self.language.add_keyword(name, move |_| Ok(build()));
2537        }
2538
2539        /// Like `add_keyword`, but the value depends on the `self` context
2540        /// property, making it not statically extractable.
2541        fn add_dynamic_keyword<O, F>(&mut self, name: &'static str, build: F)
2542        where
2543            O: 'static,
2544            F: Fn() -> O + Clone + 'static,
2545            TestTemplatePropertyKind: WrapTemplateProperty<'static, O>,
2546        {
2547            self.language.add_keyword(name, move |self_property| {
2548                let build = build.clone();
2549                Ok(self_property.map(move |_| build()).into_dyn_wrapped())
2550            });
2551        }
2552
2553        fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
2554            self.aliases_map.insert(decl, defn).unwrap();
2555        }
2556
2557        fn add_color(&mut self, label: &str, fg: crossterm::style::Color) {
2558            let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
2559            let style = formatter::Style {
2560                fg: Some(fg),
2561                ..Default::default()
2562            };
2563            self.color_rules.push((labels, style));
2564        }
2565
2566        fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, Context>> {
2567            parse(
2568                &self.language,
2569                &mut TemplateDiagnostics::new(),
2570                template,
2571                &self.aliases_map,
2572            )
2573        }
2574
2575        fn parse_err(&self, template: &str) -> String {
2576            let err = self
2577                .parse(template)
2578                .err()
2579                .expect("Got unexpected successful template rendering");
2580
2581            iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()).join("\n")
2582        }
2583
2584        fn parse_err_kind(&self, template: &str) -> TemplateParseErrorKind {
2585            self.parse(template)
2586                .err()
2587                .expect("Got unexpected successful template rendering")
2588                .kind()
2589                .clone()
2590        }
2591
2592        fn render_ok(&self, template: &str) -> String {
2593            let template = self.parse(template).unwrap();
2594            let mut output = Vec::new();
2595            let mut formatter =
2596                ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
2597            template.format(&Context, &mut formatter).unwrap();
2598            drop(formatter);
2599            String::from_utf8(output).unwrap()
2600        }
2601
2602        fn render_plain(&self, template: &str) -> String {
2603            let template = self.parse(template).unwrap();
2604            String::from_utf8(template.format_plain_text(&Context)).unwrap()
2605        }
2606    }
2607
2608    fn literal<'a, O>(value: O) -> TestTemplatePropertyKind
2609    where
2610        O: Clone + 'a,
2611        TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
2612    {
2613        Literal(value).into_dyn_wrapped()
2614    }
2615
2616    fn new_error_property<'a, O>(message: &'a str) -> TestTemplatePropertyKind
2617    where
2618        TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
2619    {
2620        Literal(())
2621            .and_then(|()| Err(TemplatePropertyError(message.into())))
2622            .into_dyn_wrapped()
2623    }
2624
2625    fn new_signature(name: &str, email: &str) -> Signature {
2626        Signature {
2627            name: name.to_owned(),
2628            email: email.to_owned(),
2629            timestamp: new_timestamp(0, 0),
2630        }
2631    }
2632
2633    fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
2634        Timestamp {
2635            timestamp: MillisSinceEpoch(msec),
2636            tz_offset,
2637        }
2638    }
2639
2640    #[test]
2641    fn test_parsed_tree() {
2642        let mut env = TestTemplateEnv::new();
2643        env.add_keyword("divergent", || literal(false));
2644        env.add_keyword("empty", || literal(true));
2645        env.add_keyword("hello", || literal("Hello".to_owned()));
2646
2647        // Empty
2648        insta::assert_snapshot!(env.render_ok(r#"  "#), @"");
2649
2650        // Single term with whitespace
2651        insta::assert_snapshot!(env.render_ok(r#"  hello.upper()  "#), @"HELLO");
2652
2653        // Multiple terms
2654        insta::assert_snapshot!(env.render_ok(r#"  hello.upper()  ++ true "#), @"HELLOtrue");
2655
2656        // Parenthesized single term
2657        insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
2658
2659        // Parenthesized multiple terms and concatenation
2660        insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
2661
2662        // Parenthesized "if" condition
2663        insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
2664
2665        // Parenthesized method chaining
2666        insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
2667
2668        // Multi-line method chaining
2669        insta::assert_snapshot!(env.render_ok("hello\n  .upper()"), @"HELLO");
2670    }
2671
2672    #[test]
2673    fn test_parse_error() {
2674        let mut env = TestTemplateEnv::new();
2675        env.add_keyword("description", || literal("".to_owned()));
2676        env.add_keyword("empty", || literal(true));
2677
2678        insta::assert_snapshot!(env.parse_err(r#"foo bar"#), @"
2679         --> 1:5
2680          |
2681        1 | foo bar
2682          |     ^---
2683          |
2684          = expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, `<`, `+`, `-`, `*`, `/`, or `%`
2685        ");
2686        insta::assert_snapshot!(env.parse_err("1 +"), @"
2687         --> 1:4
2688          |
2689        1 | 1 +
2690          |    ^---
2691          |
2692          = expected `!`, `-`, or <primary>
2693        ");
2694        insta::assert_snapshot!(env.parse_err("self.timestamp"), @"
2695         --> 1:6
2696          |
2697        1 | self.timestamp
2698          |      ^---
2699          |
2700          = expected <function>
2701        ");
2702
2703        insta::assert_snapshot!(env.parse_err(r#"foo"#), @"
2704         --> 1:1
2705          |
2706        1 | foo
2707          | ^-^
2708          |
2709          = Keyword `foo` doesn't exist
2710        ");
2711
2712        insta::assert_snapshot!(env.parse_err(r#"foo()"#), @"
2713         --> 1:1
2714          |
2715        1 | foo()
2716          | ^-^
2717          |
2718          = Function `foo` doesn't exist
2719        ");
2720        insta::assert_snapshot!(env.parse_err(r#"false()"#), @"
2721         --> 1:1
2722          |
2723        1 | false()
2724          | ^---^
2725          |
2726          = Expected identifier
2727        ");
2728
2729        insta::assert_snapshot!(env.parse_err(r#"!foo"#), @"
2730         --> 1:2
2731          |
2732        1 | !foo
2733          |  ^-^
2734          |
2735          = Keyword `foo` doesn't exist
2736        ");
2737        insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @"
2738         --> 1:9
2739          |
2740        1 | true && 123
2741          |         ^-^
2742          |
2743          = Expected expression of type `Boolean`, but actual type is `Integer`
2744        ");
2745        insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @"
2746         --> 1:1
2747          |
2748        1 | true == 1
2749          | ^-------^
2750          |
2751          = Cannot compare expressions of type `Boolean` and `Integer`
2752        ");
2753        insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @"
2754         --> 1:1
2755          |
2756        1 | true != 'a'
2757          | ^---------^
2758          |
2759          = Cannot compare expressions of type `Boolean` and `String`
2760        ");
2761        insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @"
2762         --> 1:1
2763          |
2764        1 | 1 == true
2765          | ^-------^
2766          |
2767          = Cannot compare expressions of type `Integer` and `Boolean`
2768        ");
2769        insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @"
2770         --> 1:1
2771          |
2772        1 | 1 != 'a'
2773          | ^------^
2774          |
2775          = Cannot compare expressions of type `Integer` and `String`
2776        ");
2777        insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @"
2778         --> 1:1
2779          |
2780        1 | 'a' == true
2781          | ^---------^
2782          |
2783          = Cannot compare expressions of type `String` and `Boolean`
2784        ");
2785        insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @"
2786         --> 1:1
2787          |
2788        1 | 'a' != 1
2789          | ^------^
2790          |
2791          = Cannot compare expressions of type `String` and `Integer`
2792        ");
2793        insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
2794         --> 1:1
2795          |
2796        1 | 'a' == label("", "")
2797          | ^------------------^
2798          |
2799          = Cannot compare expressions of type `String` and `Template`
2800        "#);
2801        insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @"
2802         --> 1:1
2803          |
2804        1 | 'a' > 1
2805          | ^-----^
2806          |
2807          = Cannot compare expressions of type `String` and `Integer`
2808        ");
2809
2810        insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @"
2811         --> 1:26
2812          |
2813        1 | description.first_line().foo()
2814          |                          ^-^
2815          |
2816          = Method `foo` doesn't exist for type `String`
2817        ");
2818
2819        insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @"
2820         --> 1:1
2821          |
2822        1 | 10000000000000000000
2823          | ^------------------^
2824          |
2825          = Invalid integer literal
2826        number too large to fit in target type
2827        ");
2828        insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @"
2829         --> 1:4
2830          |
2831        1 | 42.foo()
2832          |    ^-^
2833          |
2834          = Method `foo` doesn't exist for type `Integer`
2835        ");
2836        insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @"
2837         --> 1:3
2838          |
2839        1 | (-empty)
2840          |   ^---^
2841          |
2842          = Expected expression of type `Integer`, but actual type is `Boolean`
2843        ");
2844
2845        insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r#"
2846         --> 1:18
2847          |
2848        1 | ("foo" ++ "bar").baz()
2849          |                  ^-^
2850          |
2851          = Method `baz` doesn't exist for type `Template`
2852        "#);
2853
2854        insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @"
2855         --> 1:22
2856          |
2857        1 | description.contains()
2858          |                      ^
2859          |
2860          = Function `contains`: Expected 1 arguments
2861        ");
2862
2863        insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r#"
2864         --> 1:24
2865          |
2866        1 | description.first_line("foo")
2867          |                        ^---^
2868          |
2869          = Function `first_line`: Expected 0 arguments
2870        "#);
2871
2872        insta::assert_snapshot!(env.parse_err(r#"label()"#), @"
2873         --> 1:7
2874          |
2875        1 | label()
2876          |       ^
2877          |
2878          = Function `label`: Expected 2 arguments
2879        ");
2880        insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r#"
2881         --> 1:7
2882          |
2883        1 | label("foo", "bar", "baz")
2884          |       ^-----------------^
2885          |
2886          = Function `label`: Expected 2 arguments
2887        "#);
2888
2889        insta::assert_snapshot!(env.parse_err(r#"if()"#), @"
2890         --> 1:4
2891          |
2892        1 | if()
2893          |    ^
2894          |
2895          = Function `if`: Expected 2 to 3 arguments
2896        ");
2897        insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r#"
2898         --> 1:4
2899          |
2900        1 | if("foo", "bar", "baz", "quux")
2901          |    ^-------------------------^
2902          |
2903          = Function `if`: Expected 2 to 3 arguments
2904        "#);
2905
2906        insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
2907         --> 1:37
2908          |
2909        1 | pad_start("foo", fill_char = "bar", "baz")
2910          |                                     ^---^
2911          |
2912          = Function `pad_start`: Positional argument follows keyword argument
2913        "#);
2914
2915        insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r#"
2916         --> 1:4
2917          |
2918        1 | if(label("foo", "bar"), "baz")
2919          |    ^-----------------^
2920          |
2921          = Expected expression of type `Boolean`, but actual type is `Template`
2922        "#);
2923
2924        insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @"
2925         --> 1:1
2926          |
2927        1 | |x| description
2928          | ^-------------^
2929          |
2930          = Lambda cannot be defined here
2931        ");
2932    }
2933
2934    #[test]
2935    fn test_self_keyword() {
2936        let mut env = TestTemplateEnv::new();
2937        env.add_keyword("say_hello", || literal("Hello".to_owned()));
2938
2939        insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
2940        insta::assert_snapshot!(env.parse_err(r#"self"#), @"
2941         --> 1:1
2942          |
2943        1 | self
2944          | ^--^
2945          |
2946          = Expected expression of type `Template`, but actual type is `Self`
2947        ");
2948    }
2949
2950    #[test]
2951    fn test_boolean_cast() {
2952        let mut env = TestTemplateEnv::new();
2953
2954        insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
2955        insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
2956
2957        env.add_keyword("sl0", || literal::<Vec<String>>(vec![]));
2958        env.add_keyword("sl1", || literal(vec!["".to_owned()]));
2959        insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
2960        insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
2961
2962        // No implicit cast of integer
2963        insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @"
2964         --> 1:4
2965          |
2966        1 | if(0, true, false)
2967          |    ^
2968          |
2969          = Expected expression of type `Boolean`, but actual type is `Integer`
2970        ");
2971
2972        // Optional integer can be converted to boolean, and Some(0) is truthy.
2973        env.add_keyword("none_i64", || literal(None::<i64>));
2974        env.add_keyword("some_i64", || literal(Some(0)));
2975        insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
2976        insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
2977
2978        // Property errors do not evaluate
2979        insta::assert_snapshot!(
2980            env.render_ok("if(-none_i64 == 1, true, false)"),
2981            @"<Error: No Integer available>"
2982        );
2983
2984        insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r#"
2985         --> 1:4
2986          |
2987        1 | if(label("", ""), true, false)
2988          |    ^-----------^
2989          |
2990          = Expected expression of type `Boolean`, but actual type is `Template`
2991        "#);
2992        insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @"
2993         --> 1:4
2994          |
2995        1 | if(sl0.map(|x| x), true, false)
2996          |    ^------------^
2997          |
2998          = Expected expression of type `Boolean`, but actual type is `AnyList`
2999        ");
3000
3001        env.add_keyword("empty_email", || literal(Email("".to_owned())));
3002        env.add_keyword("nonempty_email", || {
3003            literal(Email("local@domain".to_owned()))
3004        });
3005        insta::assert_snapshot!(env.render_ok(r#"if(empty_email, true, false)"#), @"false");
3006        insta::assert_snapshot!(env.render_ok(r#"if(nonempty_email, true, false)"#), @"true");
3007
3008        // even boolean config values must be extracted
3009        env.add_keyword("config_bool", || literal(ConfigValue::from(true)));
3010        insta::assert_snapshot!(env.parse_err("if(config_bool, true, false)"), @"
3011         --> 1:4
3012          |
3013        1 | if(config_bool, true, false)
3014          |    ^---------^
3015          |
3016          = Expected expression of type `Boolean`, but actual type is `ConfigValue`
3017        ");
3018
3019        // misc uncastable types
3020        env.add_keyword("signature", || {
3021            literal(new_signature("Test User", "test.user@example.com"))
3022        });
3023        env.add_keyword("size_hint", || literal((5, None)));
3024        env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3025        env.add_keyword("timestamp_range", || {
3026            literal(TimestampRange {
3027                start: new_timestamp(0, 0),
3028                end: new_timestamp(0, 0),
3029            })
3030        });
3031        assert_matches!(
3032            env.parse_err_kind("if(signature, true, false)"),
3033            TemplateParseErrorKind::Expression(_)
3034        );
3035        assert_matches!(
3036            env.parse_err_kind("if(size_hint, true, false)"),
3037            TemplateParseErrorKind::Expression(_)
3038        );
3039        assert_matches!(
3040            env.parse_err_kind("if(timestamp, true, false)"),
3041            TemplateParseErrorKind::Expression(_)
3042        );
3043        assert_matches!(
3044            env.parse_err_kind("if(timestamp_range, true, false)"),
3045            TemplateParseErrorKind::Expression(_)
3046        );
3047        assert_matches!(
3048            env.parse_err_kind("if(if(true, true), true, false)"),
3049            TemplateParseErrorKind::Expression(_)
3050        );
3051        assert_matches!(
3052            env.parse_err_kind("if(sl0.map(|s| s), true, false)"),
3053            TemplateParseErrorKind::Expression(_)
3054        );
3055    }
3056
3057    #[test]
3058    fn test_arithmetic_operation() {
3059        let mut env = TestTemplateEnv::new();
3060        env.add_keyword("none_i64", || literal(None::<i64>));
3061        env.add_keyword("some_i64", || literal(Some(1)));
3062        env.add_keyword("i64_min", || literal(i64::MIN));
3063        env.add_keyword("i64_max", || literal(i64::MAX));
3064
3065        insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
3066        insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
3067        insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
3068        insta::assert_snapshot!(env.render_ok(r#"1 + 2"#), @"3");
3069        insta::assert_snapshot!(env.render_ok(r#"2 * 3"#), @"6");
3070        insta::assert_snapshot!(env.render_ok(r#"1 + 2 * 3"#), @"7");
3071        insta::assert_snapshot!(env.render_ok(r#"4 / 2"#), @"2");
3072        insta::assert_snapshot!(env.render_ok(r#"5 / 2"#), @"2");
3073        insta::assert_snapshot!(env.render_ok(r#"5 % 2"#), @"1");
3074
3075        // Since methods of the contained value can be invoked, it makes sense
3076        // to apply operators to optional integers as well.
3077        insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
3078        insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
3079        insta::assert_snapshot!(env.render_ok(r#"some_i64 + some_i64"#), @"2");
3080        insta::assert_snapshot!(env.render_ok(r#"some_i64 + none_i64"#), @"<Error: No Integer available>");
3081        insta::assert_snapshot!(env.render_ok(r#"none_i64 + some_i64"#), @"<Error: No Integer available>");
3082        insta::assert_snapshot!(env.render_ok(r#"none_i64 + none_i64"#), @"<Error: No Integer available>");
3083
3084        // No panic on integer overflow.
3085        insta::assert_snapshot!(
3086            env.render_ok(r#"-i64_min"#),
3087            @"<Error: Attempt to negate with overflow>");
3088        insta::assert_snapshot!(
3089            env.render_ok(r#"i64_max + 1"#),
3090            @"<Error: Attempt to add with overflow>");
3091        insta::assert_snapshot!(
3092            env.render_ok(r#"i64_min - 1"#),
3093            @"<Error: Attempt to subtract with overflow>");
3094        insta::assert_snapshot!(
3095            env.render_ok(r#"i64_max * 2"#),
3096            @"<Error: Attempt to multiply with overflow>");
3097        insta::assert_snapshot!(
3098            env.render_ok(r#"i64_min / -1"#),
3099            @"<Error: Attempt to divide with overflow>");
3100        insta::assert_snapshot!(
3101            env.render_ok(r#"1 / 0"#),
3102            @"<Error: Attempt to divide by zero>");
3103        insta::assert_snapshot!(
3104            env.render_ok("i64_min % -1"),
3105            @"<Error: Attempt to divide with overflow>");
3106        insta::assert_snapshot!(
3107            env.render_ok(r#"1 % 0"#),
3108            @"<Error: Attempt to divide by zero>");
3109    }
3110
3111    #[test]
3112    fn test_relational_operation() {
3113        let mut env = TestTemplateEnv::new();
3114        env.add_keyword("none_i64", || literal(None::<i64>));
3115        env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3116        env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3117
3118        insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true");
3119        insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false");
3120        insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true");
3121        insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false");
3122        insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true");
3123        insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false");
3124        insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true");
3125        insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false");
3126
3127        // none < some
3128        insta::assert_snapshot!(env.render_ok(r#"none_i64 < some_i64_0"#), @"true");
3129        insta::assert_snapshot!(env.render_ok(r#"some_i64_0 > some_i64_1"#), @"false");
3130        insta::assert_snapshot!(env.render_ok(r#"none_i64 < 0"#), @"true");
3131        insta::assert_snapshot!(env.render_ok(r#"1 > some_i64_0"#), @"true");
3132
3133        // invalid comparisons
3134        assert_matches!(
3135            env.parse_err_kind("42 >= true"),
3136            TemplateParseErrorKind::Expression(_)
3137        );
3138        assert_matches!(
3139            env.parse_err_kind("none_i64 >= true"),
3140            TemplateParseErrorKind::Expression(_)
3141        );
3142
3143        // un-comparable types
3144        env.add_keyword("str_list", || {
3145            literal(vec!["foo".to_owned(), "bar".to_owned()])
3146        });
3147        env.add_keyword("cfg_val", || {
3148            literal(ConfigValue::from_iter([("foo", "bar")]))
3149        });
3150        env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(1))));
3151        env.add_keyword("signature", || {
3152            literal(new_signature("User", "user@example.com"))
3153        });
3154        env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
3155        env.add_keyword("size_hint", || literal((10, None)));
3156        env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3157        env.add_keyword("timestamp_range", || {
3158            literal(TimestampRange {
3159                start: new_timestamp(0, 0),
3160                end: new_timestamp(0, 0),
3161            })
3162        });
3163        assert_matches!(
3164            env.parse_err_kind("'a' >= 'a'"),
3165            TemplateParseErrorKind::Expression(_)
3166        );
3167        assert_matches!(
3168            env.parse_err_kind("str_list >= str_list"),
3169            TemplateParseErrorKind::Expression(_)
3170        );
3171        assert_matches!(
3172            env.parse_err_kind("true >= true"),
3173            TemplateParseErrorKind::Expression(_)
3174        );
3175        assert_matches!(
3176            env.parse_err_kind("cfg_val >= cfg_val"),
3177            TemplateParseErrorKind::Expression(_)
3178        );
3179        assert_matches!(
3180            env.parse_err_kind("some_cfg >= some_cfg"),
3181            TemplateParseErrorKind::Expression(_)
3182        );
3183        assert_matches!(
3184            env.parse_err_kind("signature >= signature"),
3185            TemplateParseErrorKind::Expression(_)
3186        );
3187        assert_matches!(
3188            env.parse_err_kind("email >= email"),
3189            TemplateParseErrorKind::Expression(_)
3190        );
3191        assert_matches!(
3192            env.parse_err_kind("size_hint >= size_hint"),
3193            TemplateParseErrorKind::Expression(_)
3194        );
3195        assert_matches!(
3196            env.parse_err_kind("timestamp >= timestamp"),
3197            TemplateParseErrorKind::Expression(_)
3198        );
3199        assert_matches!(
3200            env.parse_err_kind("timestamp_range >= timestamp_range"),
3201            TemplateParseErrorKind::Expression(_)
3202        );
3203        assert_matches!(
3204            env.parse_err_kind("label('', '') >= label('', '')"),
3205            TemplateParseErrorKind::Expression(_)
3206        );
3207        assert_matches!(
3208            env.parse_err_kind("if(true, true) >= if(true, true)"),
3209            TemplateParseErrorKind::Expression(_)
3210        );
3211        assert_matches!(
3212            env.parse_err_kind("str_list.map(|s| s) >= str_list.map(|s| s)"),
3213            TemplateParseErrorKind::Expression(_)
3214        );
3215    }
3216
3217    #[test]
3218    fn test_logical_operation() {
3219        let mut env = TestTemplateEnv::new();
3220        env.add_keyword("none_i64", || literal::<Option<i64>>(None));
3221        env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3222        env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3223        env.add_keyword("email1", || literal(Email("local-1@domain".to_owned())));
3224        env.add_keyword("email2", || literal(Email("local-2@domain".to_owned())));
3225
3226        insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
3227        insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
3228        insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
3229        insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
3230        insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
3231        insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
3232        insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
3233
3234        insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
3235        insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
3236        insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
3237        insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
3238        insta::assert_snapshot!(env.render_ok(r#"none_i64 == none_i64"#), @"true");
3239        insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != some_i64_0"#), @"false");
3240        insta::assert_snapshot!(env.render_ok(r#"none_i64 == 0"#), @"false");
3241        insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != 0"#), @"false");
3242        insta::assert_snapshot!(env.render_ok(r#"1 == some_i64_1"#), @"true");
3243
3244        insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
3245        insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
3246        insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
3247        insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
3248        insta::assert_snapshot!(env.render_ok(r#"email1 == email1"#), @"true");
3249        insta::assert_snapshot!(env.render_ok(r#"email1 == email2"#), @"false");
3250        insta::assert_snapshot!(env.render_ok(r#"email1 == 'local-1@domain'"#), @"true");
3251        insta::assert_snapshot!(env.render_ok(r#"email1 != 'local-2@domain'"#), @"true");
3252        insta::assert_snapshot!(env.render_ok(r#"'local-1@domain' == email1"#), @"true");
3253        insta::assert_snapshot!(env.render_ok(r#"'local-2@domain' != email1"#), @"true");
3254
3255        insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
3256        insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
3257
3258        // Short-circuiting
3259        env.add_keyword("bad_bool", || new_error_property::<bool>("Bad"));
3260        insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
3261        insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
3262        insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
3263        insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
3264
3265        // Invalid comparisons
3266        assert_matches!(
3267            env.parse_err_kind("some_i64_0 == '0'"),
3268            TemplateParseErrorKind::Expression(_)
3269        );
3270        assert_matches!(
3271            env.parse_err_kind("email1 == 42"),
3272            TemplateParseErrorKind::Expression(_)
3273        );
3274
3275        // Un-comparable types
3276        env.add_keyword("str_list", || {
3277            literal(vec!["foo".to_owned(), "bar".to_owned()])
3278        });
3279        env.add_keyword("cfg_val", || {
3280            literal(ConfigValue::from_iter([("foo", "bar")]))
3281        });
3282        env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(true))));
3283        env.add_keyword("signature", || {
3284            literal(new_signature("User", "user@example.com"))
3285        });
3286        env.add_keyword("size_hint", || literal((10, None)));
3287        env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3288        env.add_keyword("timestamp_range", || {
3289            literal(TimestampRange {
3290                start: new_timestamp(0, 0),
3291                end: new_timestamp(0, 0),
3292            })
3293        });
3294        assert_matches!(
3295            env.parse_err_kind("str_list == str_list"),
3296            TemplateParseErrorKind::Expression(_)
3297        );
3298        assert_matches!(
3299            env.parse_err_kind("cfg_val == cfg_val"),
3300            TemplateParseErrorKind::Expression(_)
3301        );
3302        assert_matches!(
3303            env.parse_err_kind("some_cfg == some_cfg"),
3304            TemplateParseErrorKind::Expression(_)
3305        );
3306        assert_matches!(
3307            env.parse_err_kind("signature == signature"),
3308            TemplateParseErrorKind::Expression(_)
3309        );
3310        assert_matches!(
3311            env.parse_err_kind("size_hint == size_hint"),
3312            TemplateParseErrorKind::Expression(_)
3313        );
3314        assert_matches!(
3315            env.parse_err_kind("timestamp == timestamp"),
3316            TemplateParseErrorKind::Expression(_)
3317        );
3318        assert_matches!(
3319            env.parse_err_kind("timestamp_range == timestamp_range"),
3320            TemplateParseErrorKind::Expression(_)
3321        );
3322        assert_matches!(
3323            env.parse_err_kind("label('', '') == label('', '')"),
3324            TemplateParseErrorKind::Expression(_)
3325        );
3326        assert_matches!(
3327            env.parse_err_kind("if(true, true) == if(true, true)"),
3328            TemplateParseErrorKind::Expression(_)
3329        );
3330        assert_matches!(
3331            env.parse_err_kind("str_list.map(|s| s) == str_list.map(|s| s)"),
3332            TemplateParseErrorKind::Expression(_)
3333        );
3334    }
3335
3336    #[test]
3337    fn test_list_method() {
3338        let mut env = TestTemplateEnv::new();
3339        env.add_keyword("empty", || literal(true));
3340        env.add_keyword("sep", || literal("sep".to_owned()));
3341
3342        insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
3343        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
3344
3345        insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
3346        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
3347        // Null separator
3348        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
3349        // Keyword as separator
3350        insta::assert_snapshot!(
3351            env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
3352            @"aSEPbSEPc");
3353
3354        insta::assert_snapshot!(
3355            env.render_ok(r#""a\nbb\nc".lines().filter(|s| s.len() == 1)"#),
3356            @"a c");
3357
3358        insta::assert_snapshot!(
3359            env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
3360            @"aa bb cc");
3361
3362        // Test any() method
3363        insta::assert_snapshot!(
3364            env.render_ok(r#""a\nb\nc".lines().any(|s| s == "b")"#),
3365            @"true");
3366        insta::assert_snapshot!(
3367            env.render_ok(r#""a\nb\nc".lines().any(|s| s == "d")"#),
3368            @"false");
3369        insta::assert_snapshot!(
3370            env.render_ok(r#""".lines().any(|s| s == "a")"#),
3371            @"false");
3372        // any() with more complex predicate
3373        insta::assert_snapshot!(
3374            env.render_ok(r#""ax\nbb\nc".lines().any(|s| s.contains("x"))"#),
3375            @"true");
3376        insta::assert_snapshot!(
3377            env.render_ok(r#""a\nbb\nc".lines().any(|s| s.len() > 1)"#),
3378            @"true");
3379
3380        // Test all() method
3381        insta::assert_snapshot!(
3382            env.render_ok(r#""a\nb\nc".lines().all(|s| s.len() == 1)"#),
3383            @"true");
3384        insta::assert_snapshot!(
3385            env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() == 1)"#),
3386            @"false");
3387        // Empty list returns true for all()
3388        insta::assert_snapshot!(
3389            env.render_ok(r#""".lines().all(|s| s == "a")"#),
3390            @"true");
3391        // all() with more complex predicate
3392        insta::assert_snapshot!(
3393            env.render_ok(r#""ax\nbx\ncx".lines().all(|s| s.ends_with("x"))"#),
3394            @"true");
3395        insta::assert_snapshot!(
3396            env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() < 3)"#),
3397            @"true");
3398
3399        // Combining any/all with filter
3400        insta::assert_snapshot!(
3401            env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).any(|s| s == "bb")"#),
3402            @"true");
3403        insta::assert_snapshot!(
3404            env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).all(|s| s.len() >= 2)"#),
3405            @"true");
3406
3407        // Nested any/all operations
3408        insta::assert_snapshot!(
3409            env.render_ok(r#"if("a\nb".lines().any(|s| s == "a"), "found", "not found")"#),
3410            @"found");
3411        insta::assert_snapshot!(
3412            env.render_ok(r#"if("a\nb".lines().all(|s| s.len() == 1), "all single", "not all")"#),
3413            @"all single");
3414
3415        // Global keyword in item template
3416        insta::assert_snapshot!(
3417            env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
3418            @"atrue btrue ctrue");
3419        // Global keyword in item template shadowing 'self'
3420        insta::assert_snapshot!(
3421            env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
3422            @"atrue btrue ctrue");
3423        // Override global keyword 'empty'
3424        insta::assert_snapshot!(
3425            env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
3426            @"a b c");
3427        // Nested map operations
3428        insta::assert_snapshot!(
3429            env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
3430            @"ax ay bx by cx cy");
3431        // Nested map/join operations
3432        insta::assert_snapshot!(
3433            env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
3434            @"ax,ay;bx,by;cx,cy");
3435        // Nested string operations
3436        insta::assert_snapshot!(
3437            env.render_ok(r#""!  a\n!b\nc\n   end".remove_suffix("end").trim_end().lines().map(|s| s.remove_prefix("!").trim_start())"#),
3438            @"a b c");
3439
3440        // Lambda expression in alias
3441        env.add_alias("identity", "|x| x");
3442        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
3443
3444        // Not a lambda expression
3445        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r#"
3446         --> 1:17
3447          |
3448        1 | "a".lines().map(empty)
3449          |                 ^---^
3450          |
3451          = Expected lambda expression
3452        "#);
3453        // Bad lambda parameter count
3454        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r#"
3455         --> 1:18
3456          |
3457        1 | "a".lines().map(|| "")
3458          |                  ^
3459          |
3460          = Expected 1 lambda parameters
3461        "#);
3462        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r#"
3463         --> 1:18
3464          |
3465        1 | "a".lines().map(|a, b| "")
3466          |                  ^--^
3467          |
3468          = Expected 1 lambda parameters
3469        "#);
3470        // Bad lambda output
3471        insta::assert_snapshot!(env.parse_err(r#""a".lines().filter(|s| s ++ "\n")"#), @r#"
3472         --> 1:24
3473          |
3474        1 | "a".lines().filter(|s| s ++ "\n")
3475          |                        ^-------^
3476          |
3477          = Expected expression of type `Boolean`, but actual type is `Template`
3478        "#);
3479
3480        // Error in any() and all()
3481        insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|s| s.len())"#), @r#"
3482         --> 1:21
3483          |
3484        1 | "a".lines().any(|s| s.len())
3485          |                     ^-----^
3486          |
3487          = Expected expression of type `Boolean`, but actual type is `Integer`
3488        "#);
3489        // Bad lambda output for all()
3490        insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|s| s ++ "x")"#), @r#"
3491         --> 1:21
3492          |
3493        1 | "a".lines().all(|s| s ++ "x")
3494          |                     ^------^
3495          |
3496          = Expected expression of type `Boolean`, but actual type is `Template`
3497        "#);
3498        // Wrong parameter count for any()
3499        insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|| true)"#), @r#"
3500         --> 1:18
3501          |
3502        1 | "a".lines().any(|| true)
3503          |                  ^
3504          |
3505          = Expected 1 lambda parameters
3506        "#);
3507        // Wrong parameter count for all()
3508        insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|a, b| true)"#), @r#"
3509         --> 1:18
3510          |
3511        1 | "a".lines().all(|a, b| true)
3512          |                  ^--^
3513          |
3514          = Expected 1 lambda parameters
3515        "#);
3516        // Error in lambda expression
3517        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r#"
3518         --> 1:23
3519          |
3520        1 | "a".lines().map(|s| s.unknown())
3521          |                       ^-----^
3522          |
3523          = Method `unknown` doesn't exist for type `String`
3524        "#);
3525        // Error in lambda alias
3526        env.add_alias("too_many_params", "|x, y| x");
3527        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
3528         --> 1:17
3529          |
3530        1 | "a".lines().map(too_many_params)
3531          |                 ^-------------^
3532          |
3533          = In alias `too_many_params`
3534         --> 1:2
3535          |
3536        1 | |x, y| x
3537          |  ^--^
3538          |
3539          = Expected 1 lambda parameters
3540        "#);
3541
3542        // List.first()
3543        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().first()"#), @"a");
3544        insta::assert_snapshot!(env.render_ok(r#""".lines().first()"#), @"<Error: List is empty>");
3545
3546        // List.last()
3547        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().last()"#), @"c");
3548        insta::assert_snapshot!(env.render_ok(r#""".lines().last()"#), @"<Error: List is empty>");
3549
3550        // List.get(index)
3551        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(0)"#), @"a");
3552        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(1)"#), @"b");
3553        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(2)"#), @"c");
3554        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(3)"#), @"<Error: Index 3 out of bounds>");
3555        insta::assert_snapshot!(env.render_ok(r#""".lines().get(0)"#), @"<Error: Index 0 out of bounds>");
3556
3557        // List.reverse()
3558        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().reverse().join("|")"#), @"c|b|a");
3559        insta::assert_snapshot!(env.render_ok(r#""".lines().reverse().join("|")"#), @"");
3560
3561        // List.skip(count)
3562        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(0).join("|")"#), @"a|b|c");
3563        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(1).join("|")"#), @"b|c");
3564        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(2).join("|")"#), @"c");
3565        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(3).join("|")"#), @"");
3566        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(10).join("|")"#), @"");
3567
3568        // List.take(count)
3569        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(0).join("|")"#), @"");
3570        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(1).join("|")"#), @"a");
3571        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(2).join("|")"#), @"a|b");
3572        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(3).join("|")"#), @"a|b|c");
3573        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(10).join("|")"#), @"a|b|c");
3574
3575        // Combining skip and take
3576        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\nd".lines().skip(1).take(2).join("|")"#), @"b|c");
3577    }
3578
3579    #[test]
3580    fn test_string_method() {
3581        let mut env = TestTemplateEnv::new();
3582        env.add_keyword("description", || literal("description 1".to_owned()));
3583        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
3584
3585        insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
3586        insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
3587        insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
3588
3589        insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
3590        insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
3591        insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
3592        insta::assert_snapshot!(
3593            env.render_ok(r#""description 123".contains(description.first_line())"#),
3594            @"true");
3595
3596        // String patterns are not stringifiable
3597        insta::assert_snapshot!(env.parse_err(r#""fa".starts_with(regex:'[a-f]o+')"#), @r#"
3598         --> 1:18
3599          |
3600        1 | "fa".starts_with(regex:'[a-f]o+')
3601          |                  ^-------------^
3602          |
3603          = String patterns may not be used as expression values
3604        "#);
3605
3606        // inner template error should propagate
3607        insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
3608        insta::assert_snapshot!(
3609            env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
3610        insta::assert_snapshot!(
3611            env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
3612
3613        insta::assert_snapshot!(env.render_ok(r#""fooo".match(regex:'[a-f]o+')"#), @"fooo");
3614        insta::assert_snapshot!(env.render_ok(r#""fa".match(regex:'[a-f]o+')"#), @"");
3615        insta::assert_snapshot!(env.render_ok(r#""hello".match(regex:"h(ell)o")"#), @"hello");
3616        insta::assert_snapshot!(env.render_ok(r#""HEllo".match(regex-i:"h(ell)o")"#), @"HEllo");
3617        insta::assert_snapshot!(env.render_ok(r#""hEllo".match(glob:"h*o")"#), @"hEllo");
3618        insta::assert_snapshot!(env.render_ok(r#""Hello".match(glob:"h*o")"#), @"");
3619        insta::assert_snapshot!(env.render_ok(r#""HEllo".match(glob-i:"h*o")"#), @"HEllo");
3620        insta::assert_snapshot!(env.render_ok(r#""hello".match("he")"#), @"he");
3621        insta::assert_snapshot!(env.render_ok(r#""hello".match(substring:"he")"#), @"he");
3622        insta::assert_snapshot!(env.render_ok(r#""hello".match(exact:"he")"#), @"");
3623
3624        // Evil regexes can cause invalid UTF-8 output, which nothing can
3625        // really be done about given we're matching against non-UTF-8 stuff a
3626        // lot as well.
3627        insta::assert_snapshot!(env.render_ok(r#""🥺".match(regex:'(?-u)^(?:.)')"#), @"<Error: incomplete utf-8 byte sequence from index 0>");
3628
3629        insta::assert_snapshot!(env.parse_err(r#""hello".match(false)"#), @r#"
3630         --> 1:15
3631          |
3632        1 | "hello".match(false)
3633          |               ^---^
3634          |
3635          = Expected string pattern
3636        "#);
3637        insta::assert_snapshot!(env.parse_err(r#""🥺".match(not-a-pattern:"abc")"#), @r#"
3638         --> 1:11
3639          |
3640        1 | "🥺".match(not-a-pattern:"abc")
3641          |           ^-----------------^
3642          |
3643          = Bad string pattern
3644        Invalid string pattern kind `not-a-pattern:`
3645        "#);
3646
3647        insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
3648        insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
3649
3650        insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
3651        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
3652
3653        insta::assert_snapshot!(env.render_ok(r#""".split(",")"#), @"");
3654        insta::assert_snapshot!(env.render_ok(r#""a,b,c".split(",")"#), @"a b c");
3655        insta::assert_snapshot!(env.render_ok(r#""a::b::c::d".split("::")"#), @"a b c d");
3656        insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 0)"#), @"");
3657        insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 2)"#), @"a b,c,d");
3658        insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 3)"#), @"a b c,d");
3659        insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 10)"#), @"a b c d");
3660        insta::assert_snapshot!(env.render_ok(r#""abc".split(",", -1)"#), @"<Error: out of range integral type conversion attempted>");
3661        insta::assert_snapshot!(env.render_ok(r#"json("a1b2c3".split(regex:'\d+'))"#), @r#"["a","b","c",""]"#);
3662        insta::assert_snapshot!(env.render_ok(r#""foo  bar   baz".split(regex:'\s+')"#), @"foo bar baz");
3663        insta::assert_snapshot!(env.render_ok(r#""a1b2c3d4".split(regex:'\d+', 3)"#), @"a b c3d4");
3664        insta::assert_snapshot!(env.render_ok(r#"json("hello world".split(regex-i:"WORLD"))"#), @r#"["hello ",""]"#);
3665
3666        insta::assert_snapshot!(env.render_ok("''.upper()"), @"");
3667        insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.upper()"), @"ABCABC 123!@#");
3668        insta::assert_snapshot!(env.render_ok("''.lower()"), @"");
3669        insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.lower()"), @"abcabc 123!@#");
3670
3671        insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
3672        insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
3673        insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
3674        insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
3675        insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
3676        insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
3677
3678        insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
3679        insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
3680        insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
3681        insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
3682        insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
3683        insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
3684
3685        insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
3686        insta::assert_snapshot!(
3687            env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
3688            @"testing");
3689
3690        insta::assert_snapshot!(
3691            env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
3692            @"bar@my.example.com");
3693        insta::assert_snapshot!(
3694            env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
3695            @"bar");
3696
3697        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim()"#), @"");
3698        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim()"#), @"foo  bar");
3699
3700        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim_start()"#), @"");
3701        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim_start()"#), @"foo  bar");
3702
3703        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim_end()"#), @"");
3704        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim_end()"#), @"\n\r foo  bar");
3705
3706        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
3707        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
3708        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
3709        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
3710        insta::assert_snapshot!(env.render_ok(r#""foo".substr(1, 3)"#), @"oo");
3711        insta::assert_snapshot!(env.render_ok(r#""foo".substr(1)"#), @"oo");
3712        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0)"#), @"foo");
3713        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
3714        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
3715        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3)"#), @"def");
3716        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
3717        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
3718        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-100)"#), @"abcdef");
3719
3720        // non-ascii characters
3721        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
3722        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
3723        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
3724        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
3725        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
3726        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
3727        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
3728        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
3729        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
3730        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
3731        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
3732        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(0)"#), @"abc💩");
3733        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(1)"#), @"bc💩");
3734        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3)"#), @"💩");
3735        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(4)"#), @"💩");
3736        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-5)"#), @"c💩");
3737        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4)"#), @"💩");
3738        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3)"#), @"");
3739        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-2)"#), @"");
3740        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1)"#), @"");
3741
3742        // ranges with end > start are empty
3743        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
3744        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
3745
3746        insta::assert_snapshot!(env.render_ok(r#""hello".escape_json()"#), @r#""hello""#);
3747        insta::assert_snapshot!(env.render_ok(r#""he \n ll \n \" o".escape_json()"#), @r#""he \n ll \n \" o""#);
3748
3749        // simple substring replacement
3750        insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", "jj")"#), @"hello jj");
3751        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj")"#), @"hello jj jj");
3752        insta::assert_snapshot!(env.render_ok(r#""hello".replace("missing", "jj")"#), @"hello");
3753
3754        // replace with limit >=0
3755        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 0)"#), @"hello world world");
3756        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 1)"#), @"hello jj world");
3757        insta::assert_snapshot!(env.render_ok(r#""hello world world world".replace("world", "jj", 2)"#), @"hello jj jj world");
3758
3759        // replace with limit <0 (error due to negative limit)
3760        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -1)"#), @"<Error: out of range integral type conversion attempted>");
3761        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -5)"#), @"<Error: out of range integral type conversion attempted>");
3762
3763        // replace with regex patterns
3764        insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X")"#), @"helloXworldX");
3765        insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X", 1)"#), @"helloXworld456");
3766
3767        // replace with regex patterns (capture groups)
3768        insta::assert_snapshot!(env.render_ok(r#""HELLO    WORLD".replace(regex-i:"(hello) +(world)", "$2 $1")"#), @"WORLD HELLO");
3769        insta::assert_snapshot!(env.render_ok(r#""abc123".replace(regex:"([a-z]+)([0-9]+)", "$2-$1")"#), @"123-abc");
3770        insta::assert_snapshot!(env.render_ok(r#""foo123bar".replace(regex:'\d+', "[$0]")"#), @"foo[123]bar");
3771
3772        // replace with regex patterns (case insensitive)
3773        insta::assert_snapshot!(env.render_ok(r#""Hello World".replace(regex-i:"hello", "hi")"#), @"hi World");
3774        insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi")"#), @"hi World hi");
3775        insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi", 1)"#), @"hi World Hello");
3776
3777        // replace with strings that look regex-y ($n patterns are always expanded)
3778        insta::assert_snapshot!(env.render_ok(r#"'hello\d+world'.replace('\d+', "X")"#), @"helloXworld");
3779        insta::assert_snapshot!(env.render_ok(r#""(foo)($1)bar".replace("$1", "$2")"#), @"(foo)()bar");
3780        insta::assert_snapshot!(env.render_ok(r#""test(abc)end".replace("(abc)", "X")"#), @"testXend");
3781
3782        // replace with templates
3783        insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", description.first_line())"#), @"hello description 1");
3784
3785        // replace with error
3786        insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", bad_string)"#), @"<Error: Bad>");
3787    }
3788
3789    #[test]
3790    fn test_config_value_method() {
3791        let mut env = TestTemplateEnv::new();
3792        env.add_keyword("boolean", || literal(ConfigValue::from(true)));
3793        env.add_keyword("integer", || literal(ConfigValue::from(42)));
3794        env.add_keyword("string", || literal(ConfigValue::from("foo")));
3795        env.add_keyword("string_list", || {
3796            literal(ConfigValue::from_iter(["foo", "bar"]))
3797        });
3798
3799        insta::assert_snapshot!(env.render_ok("boolean"), @"true");
3800        insta::assert_snapshot!(env.render_ok("integer"), @"42");
3801        insta::assert_snapshot!(env.render_ok("string"), @r#""foo""#);
3802        insta::assert_snapshot!(env.render_ok("string_list"), @r#"["foo", "bar"]"#);
3803
3804        insta::assert_snapshot!(env.render_ok("boolean.as_boolean()"), @"true");
3805        insta::assert_snapshot!(env.render_ok("integer.as_integer()"), @"42");
3806        insta::assert_snapshot!(env.render_ok("string.as_string()"), @"foo");
3807        insta::assert_snapshot!(env.render_ok("string_list.as_string_list()"), @"foo bar");
3808
3809        insta::assert_snapshot!(
3810            env.render_ok("boolean.as_integer()"),
3811            @"<Error: invalid type: boolean `true`, expected i64>");
3812        insta::assert_snapshot!(
3813            env.render_ok("integer.as_string()"),
3814            @"<Error: invalid type: integer `42`, expected a string>");
3815        insta::assert_snapshot!(
3816            env.render_ok("string.as_string_list()"),
3817            @r#"<Error: invalid type: string "foo", expected a sequence>"#);
3818        insta::assert_snapshot!(
3819            env.render_ok("string_list.as_boolean()"),
3820            @"<Error: invalid type: sequence, expected a boolean>");
3821    }
3822
3823    #[test]
3824    fn test_signature_and_email_methods() {
3825        let mut env = TestTemplateEnv::new();
3826
3827        env.add_keyword("author", || {
3828            literal(new_signature("Test User", "test.user@example.com"))
3829        });
3830        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
3831        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3832        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3833        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3834        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
3835        insta::assert_snapshot!(env.render_ok("author.timestamp()"), @"1970-01-01 00:00:00.000 +00:00");
3836
3837        env.add_keyword("author", || {
3838            literal(new_signature("Another Test User", "test.user@example.com"))
3839        });
3840        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
3841        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
3842        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3843
3844        env.add_keyword("author", || {
3845            literal(new_signature("Test User", "test.user@invalid@example.com"))
3846        });
3847        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
3848        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3849        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
3850        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3851        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"invalid@example.com");
3852
3853        env.add_keyword("author", || {
3854            literal(new_signature("Test User", "test.user"))
3855        });
3856        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
3857        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
3858        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3859        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
3860
3861        env.add_keyword("author", || {
3862            literal(new_signature("Test User", "test.user+tag@example.com"))
3863        });
3864        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
3865        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
3866        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user+tag");
3867        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
3868
3869        env.add_keyword("author", || literal(new_signature("Test User", "x@y")));
3870        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
3871        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
3872        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"x");
3873        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"y");
3874
3875        env.add_keyword("author", || {
3876            literal(new_signature("", "test.user@example.com"))
3877        });
3878        insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
3879        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
3880        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3881
3882        env.add_keyword("author", || literal(new_signature("Test User", "")));
3883        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
3884        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3885        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
3886        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"");
3887        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
3888
3889        env.add_keyword("author", || literal(new_signature("", "")));
3890        insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
3891        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
3892        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
3893    }
3894
3895    #[test]
3896    fn test_size_hint_method() {
3897        let mut env = TestTemplateEnv::new();
3898
3899        env.add_keyword("unbounded", || literal((5, None)));
3900        insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
3901        insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
3902        insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
3903        insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
3904
3905        env.add_keyword("bounded", || literal((0, Some(10))));
3906        insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
3907        insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
3908        insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
3909        insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
3910
3911        env.add_keyword("zero", || literal((0, Some(0))));
3912        insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
3913        insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
3914        insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
3915        insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
3916    }
3917
3918    #[test]
3919    fn test_timestamp_method() {
3920        let mut env = TestTemplateEnv::new();
3921        env.add_keyword("now", || literal(Timestamp::now()));
3922        env.add_keyword("t0", || literal(new_timestamp(0, 0)));
3923        env.add_keyword("t0_plus1", || literal(new_timestamp(0, 60)));
3924        env.add_keyword("tmax", || literal(new_timestamp(i64::MAX, 0)));
3925
3926        // Unformattable timestamp
3927        insta::assert_snapshot!(env.render_ok("tmax"),
3928            @"<Error: Out-of-range date>");
3929
3930        insta::assert_snapshot!(
3931            env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
3932            @"19700101 00:00:00");
3933
3934        // Invalid format string
3935        insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r#"
3936         --> 1:11
3937          |
3938        1 | t0.format("%_")
3939          |           ^--^
3940          |
3941          = Invalid time format
3942        "#);
3943
3944        // Invalid type
3945        insta::assert_snapshot!(env.parse_err(r#"t0.format(0)"#), @"
3946         --> 1:11
3947          |
3948        1 | t0.format(0)
3949          |           ^
3950          |
3951          = Expected string literal
3952        ");
3953
3954        // Dynamic string isn't supported yet
3955        insta::assert_snapshot!(env.parse_err(r#"t0.format("%Y" ++ "%m")"#), @r#"
3956         --> 1:11
3957          |
3958        1 | t0.format("%Y" ++ "%m")
3959          |           ^----------^
3960          |
3961          = Expected string literal
3962        "#);
3963
3964        // Literal alias expansion
3965        env.add_alias("time_format", r#""%Y-%m-%d""#);
3966        env.add_alias("bad_time_format", r#""%_""#);
3967        insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
3968        insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
3969         --> 1:11
3970          |
3971        1 | t0.format(bad_time_format)
3972          |           ^-------------^
3973          |
3974          = In alias `bad_time_format`
3975         --> 1:1
3976          |
3977        1 | "%_"
3978          | ^--^
3979          |
3980          = Invalid time format
3981        "#);
3982
3983        insta::assert_snapshot!(env.render_ok("t0_plus1.utc()"), @"1970-01-01 00:00:00.000 +00:00");
3984
3985        // TODO: exercise ago() and local() deterministically
3986        // Just make sure these methods work for now
3987        assert!(!env.render_ok("now.ago()").is_empty());
3988        assert!(!env.render_ok("now.local()").is_empty());
3989
3990        insta::assert_snapshot!(env.render_ok("t0.after('1969')"), @"true");
3991        insta::assert_snapshot!(env.render_ok("t0.before('1969')"), @"false");
3992        insta::assert_snapshot!(env.render_ok("t0.after('now')"), @"false");
3993        insta::assert_snapshot!(env.render_ok("t0.before('now')"), @"true");
3994        insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
3995         --> 1:11
3996          |
3997        1 | t0.before('invalid')
3998          |           ^-------^
3999          |
4000          = Invalid date pattern
4001        expected unsupported identifier as position 0..7
4002        ");
4003        insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
4004         --> 1:11
4005          |
4006        1 | t0.before('invalid')
4007          |           ^-------^
4008          |
4009          = Invalid date pattern
4010        expected unsupported identifier as position 0..7
4011        ");
4012
4013        // Can only compare timestamps against string literals
4014        insta::assert_snapshot!(env.parse_err("t0.after(t0)"), @"
4015         --> 1:10
4016          |
4017        1 | t0.after(t0)
4018          |          ^^
4019          |
4020          = Expected string literal
4021        ");
4022        insta::assert_snapshot!(env.parse_err("t0.before(t0)"), @"
4023         --> 1:11
4024          |
4025        1 | t0.before(t0)
4026          |           ^^
4027          |
4028          = Expected string literal
4029        ");
4030
4031        insta::assert_snapshot!(env.render_ok("t0.since(t0_plus1)"), @"1970-01-01 01:00:00.000 +01:00 - 1970-01-01 00:00:00.000 +00:00");
4032        insta::assert_snapshot!(env.render_ok("t0_plus1.since(t0)"), @"1970-01-01 00:00:00.000 +00:00 - 1970-01-01 01:00:00.000 +01:00");
4033        insta::assert_snapshot!(env.parse_err("t0.since(false)"), @"
4034         --> 1:10
4035          |
4036        1 | t0.since(false)
4037          |          ^---^
4038          |
4039          = Expected expression of type `Timestamp`, but actual type is `Boolean`
4040        ");
4041    }
4042
4043    #[test]
4044    fn test_timestamp_range_method() {
4045        let mut env = TestTemplateEnv::new();
4046        env.add_keyword("instant", || {
4047            literal(TimestampRange {
4048                start: new_timestamp(0, 0),
4049                end: new_timestamp(0, 0),
4050            })
4051        });
4052        env.add_keyword("one_msec", || {
4053            literal(TimestampRange {
4054                start: new_timestamp(0, 0),
4055                end: new_timestamp(1, -60),
4056            })
4057        });
4058
4059        insta::assert_snapshot!(
4060            env.render_ok("instant.start().format('%Y%m%d %H:%M:%S %Z')"),
4061            @"19700101 00:00:00 +00:00");
4062        insta::assert_snapshot!(
4063            env.render_ok("one_msec.end().format('%Y%m%d %H:%M:%S %Z')"),
4064            @"19691231 23:00:00 -01:00");
4065
4066        insta::assert_snapshot!(
4067            env.render_ok("instant.duration()"), @"less than a microsecond");
4068        insta::assert_snapshot!(
4069            env.render_ok("one_msec.duration()"), @"1 millisecond");
4070    }
4071
4072    #[test]
4073    fn test_fill_function() {
4074        let mut env = TestTemplateEnv::new();
4075        env.add_color("error", crossterm::style::Color::DarkRed);
4076
4077        insta::assert_snapshot!(
4078            env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
4079                                  label("error", "lazy") ++ " dog\n")"#),
4080            @"
4081        The quick fox jumps
4082        over the lazy dog
4083        ");
4084
4085        // A low value will not chop words, but can chop a label by words
4086        insta::assert_snapshot!(
4087            env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
4088                                  label("error", "longlonglongword and short words") ++
4089                                  " back out\n")"#),
4090            @"
4091        Longlonglongword
4092        an some
4093        short
4094        words
4095        longlonglongword
4096        and short
4097        words
4098        back out
4099        ");
4100
4101        // Filling to 0 means breaking at every word
4102        insta::assert_snapshot!(
4103            env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
4104                                  label("error", "lazy") ++ " dog\n")"#),
4105            @"
4106        The
4107        quick
4108        fox
4109        jumps
4110        over
4111        the
4112        lazy
4113        dog
4114        ");
4115
4116        // Filling to -0 is the same as 0
4117        insta::assert_snapshot!(
4118            env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
4119                                  label("error", "lazy") ++ " dog\n")"#),
4120            @"
4121        The
4122        quick
4123        fox
4124        jumps
4125        over
4126        the
4127        lazy
4128        dog
4129        ");
4130
4131        // Filling to negative width is an error
4132        insta::assert_snapshot!(
4133            env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
4134                                  label("error", "lazy") ++ " dog\n")"#),
4135            @"<Error: out of range integral type conversion attempted>");
4136
4137        // Word-wrap, then indent
4138        insta::assert_snapshot!(
4139            env.render_ok(r#""START marker to help insta\n" ++
4140                             indent("    ", fill(20, "The quick fox jumps over the " ++
4141                                                 label("error", "lazy") ++ " dog\n"))"#),
4142            @"
4143        START marker to help insta
4144            The quick fox jumps
4145            over the lazy dog
4146        ");
4147
4148        // Word-wrap indented (no special handling for leading spaces)
4149        insta::assert_snapshot!(
4150            env.render_ok(r#""START marker to help insta\n" ++
4151                             fill(20, indent("    ", "The quick fox jumps over the " ++
4152                                             label("error", "lazy") ++ " dog\n"))"#),
4153            @"
4154        START marker to help insta
4155            The quick fox
4156        jumps over the lazy
4157        dog
4158        ");
4159    }
4160
4161    #[test]
4162    fn test_indent_function() {
4163        let mut env = TestTemplateEnv::new();
4164        env.add_color("error", crossterm::style::Color::DarkRed);
4165        env.add_color("warning", crossterm::style::Color::DarkYellow);
4166        env.add_color("hint", crossterm::style::Color::DarkCyan);
4167
4168        // Empty line shouldn't be indented. Not using insta here because we test
4169        // whitespace existence.
4170        assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
4171        assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
4172        assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
4173
4174        // "\n" at end of labeled text
4175        insta::assert_snapshot!(
4176            env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
4177            @"
4178        __a
4179        __b
4180        ");
4181
4182        // "\n" in labeled text
4183        insta::assert_snapshot!(
4184            env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
4185            @"
4186        __ab
4187        __c
4188        ");
4189
4190        // Labeled prefix + unlabeled content
4191        insta::assert_snapshot!(
4192            env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
4193            @"
4194        XXa
4195        XXb
4196        ");
4197
4198        // Nested indent, silly but works
4199        insta::assert_snapshot!(
4200            env.render_ok(r#"indent(label("hint", "A"),
4201                                    label("warning", indent(label("hint", "B"),
4202                                                            label("error", "x\n") ++ "y")))"#),
4203            @"
4204        ABx
4205        ABy
4206        ");
4207    }
4208
4209    #[test]
4210    fn test_pad_function() {
4211        let mut env = TestTemplateEnv::new();
4212        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4213        env.add_color("red", crossterm::style::Color::Red);
4214        env.add_color("cyan", crossterm::style::Color::DarkCyan);
4215
4216        // Default fill_char is ' '
4217        insta::assert_snapshot!(
4218            env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
4219            @"{  foo}");
4220        insta::assert_snapshot!(
4221            env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
4222            @"{foo  }");
4223        insta::assert_snapshot!(
4224            env.render_ok(r"'{' ++ pad_centered(5, label('red', 'foo')) ++ '}'"),
4225            @"{ foo }");
4226
4227        // Labeled fill char
4228        insta::assert_snapshot!(
4229            env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4230            @"==foo");
4231        insta::assert_snapshot!(
4232            env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4233            @"foo==");
4234        insta::assert_snapshot!(
4235            env.render_ok(r"pad_centered(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4236            @"=foo=");
4237
4238        // Error in fill char: the output looks odd (because the error message
4239        // isn't 1-width character), but is still readable.
4240        insta::assert_snapshot!(
4241            env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
4242            @"foo");
4243        insta::assert_snapshot!(
4244            env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
4245            @"foo<<Error: Error: Bad>Bad>");
4246        insta::assert_snapshot!(
4247            env.render_ok(r"pad_centered(5, 'foo', fill_char=bad_string)"),
4248            @"<Error: Bad>foo<Error: Bad>");
4249
4250        // Invalid pad width is not a parse error
4251        insta::assert_snapshot!(
4252            env.render_ok("pad_start(-1, 'foo')"),
4253            @"<Error: out of range integral type conversion attempted>");
4254    }
4255
4256    #[test]
4257    fn test_hash_function() {
4258        let mut env = TestTemplateEnv::new();
4259        env.add_color("red", crossterm::style::Color::Red);
4260
4261        // hash is currently of stringified content
4262        // NOTE: hash algo and per-type behavior are not codified requirements
4263        assert_eq!(env.render_ok("hash(false)"), env.render_ok("hash('false')"));
4264        assert_eq!(env.render_ok("hash(0)"), env.render_ok("hash('0')"));
4265        assert_eq!(
4266            env.render_ok("hash(0)"),
4267            env.render_ok("hash(label('red', '0'))")
4268        );
4269    }
4270
4271    #[test]
4272    fn test_truncate_function() {
4273        let mut env = TestTemplateEnv::new();
4274        env.add_color("red", crossterm::style::Color::Red);
4275
4276        insta::assert_snapshot!(
4277            env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
4278            @"arbaz");
4279        insta::assert_snapshot!(
4280            env.render_ok("truncate_start(5, 'foo', 'bar')"), @"foo");
4281        insta::assert_snapshot!(
4282            env.render_ok("truncate_start(9, 'foobarbazquux', 'dotdot')"), @"dotdotuux");
4283
4284        insta::assert_snapshot!(
4285            env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
4286            @"fobaz");
4287        insta::assert_snapshot!(
4288            env.render_ok("truncate_end(5, 'foo', 'bar')"), @"foo");
4289        insta::assert_snapshot!(
4290            env.render_ok("truncate_end(9, 'foobarbazquux', 'dotdot')"), @"foodotdot");
4291
4292        // invalid truncate width is not a parse error
4293        insta::assert_snapshot!(
4294            env.render_ok("truncate_end(-1, 'foo')"),
4295            @"<Error: out of range integral type conversion attempted>");
4296    }
4297
4298    #[test]
4299    fn test_label_function() {
4300        let mut env = TestTemplateEnv::new();
4301        env.add_keyword("empty", || literal(true));
4302        env.add_color("error", crossterm::style::Color::DarkRed);
4303        env.add_color("warning", crossterm::style::Color::DarkYellow);
4304
4305        // Literal
4306        insta::assert_snapshot!(
4307            env.render_ok(r#"label("error", "text")"#),
4308            @"text");
4309
4310        // Evaluated property
4311        insta::assert_snapshot!(
4312            env.render_ok(r#"label("error".first_line(), "text")"#),
4313            @"text");
4314
4315        // Property evaluation error
4316        insta::assert_snapshot!(
4317            env.render_ok("label(fill(-1, 'foo'), 'text')"),
4318            @"<Error: out of range integral type conversion attempted>");
4319
4320        // Template
4321        insta::assert_snapshot!(
4322            env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
4323            @"text");
4324    }
4325
4326    #[test]
4327    fn test_raw_escape_sequence_function_strip_labels() {
4328        let mut env = TestTemplateEnv::new();
4329        env.add_color("error", crossterm::style::Color::DarkRed);
4330        env.add_color("warning", crossterm::style::Color::DarkYellow);
4331
4332        insta::assert_snapshot!(
4333            env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
4334            @"text",
4335        );
4336    }
4337
4338    #[test]
4339    fn test_raw_escape_sequence_function_ansi_escape() {
4340        let env = TestTemplateEnv::new();
4341
4342        // Sanitize ANSI escape without raw_escape_sequence
4343        insta::assert_snapshot!(env.render_ok(r#""\e""#), @"␛");
4344        insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"␛");
4345        insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"␛");
4346        insta::assert_snapshot!(
4347            env.render_ok(r#""]8;;"
4348                ++ "http://example.com"
4349                ++ "\e\\"
4350                ++ "Example"
4351                ++ "\x1b]8;;\x1B\\""#),
4352            @r"␛]8;;http://example.com␛\Example␛]8;;␛\");
4353
4354        // Don't sanitize ANSI escape with raw_escape_sequence
4355        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
4356        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
4357        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
4358        insta::assert_snapshot!(
4359            env.render_ok(r#"raw_escape_sequence("]8;;"
4360                ++ "http://example.com"
4361                ++ "\e\\"
4362                ++ "Example"
4363                ++ "\x1b]8;;\x1B\\")"#),
4364            @r"]8;;http://example.com\Example]8;;\");
4365    }
4366
4367    #[test]
4368    fn test_hyperlink_function_with_color() {
4369        let mut env = TestTemplateEnv::new();
4370        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4371        // With ColorFormatter, hyperlink emits OSC 8 escape sequences
4372        insta::assert_snapshot!(
4373            env.render_ok(r#"hyperlink("http://example.com", "Example")"#),
4374            @r"]8;;http://example.com\Example]8;;\");
4375        insta::assert_snapshot!(
4376            env.render_ok(r#"hyperlink(bad_string, "Example")"#),
4377            @"<Error: Bad>");
4378    }
4379
4380    #[test]
4381    fn test_hyperlink_function_without_color() {
4382        let env = TestTemplateEnv::new();
4383        // With PlainTextFormatter, hyperlink shows just the text
4384        insta::assert_snapshot!(
4385            env.render_plain(r#"hyperlink("http://example.com", "Example")"#),
4386            @"Example");
4387    }
4388
4389    #[test]
4390    fn test_hyperlink_function_custom_fallback() {
4391        let env = TestTemplateEnv::new();
4392        // Custom fallback is used when not outputting to color terminal
4393        insta::assert_snapshot!(
4394            env.render_plain(r#"hyperlink("http://example.com", "Example", "URL: http://example.com")"#),
4395            @"URL: http://example.com");
4396    }
4397
4398    #[test]
4399    fn test_hyperlink_function_stringify() {
4400        let env = TestTemplateEnv::new();
4401        // stringify() strips hyperlink to just text
4402        insta::assert_snapshot!(
4403            env.render_ok(r#"stringify(hyperlink("http://example.com", "Example"))"#),
4404            @"Example");
4405        // stringify then can be manipulated as plain text
4406        insta::assert_snapshot!(
4407            env.render_ok(r#"stringify(hyperlink("http://example.com", "Example")).upper()"#),
4408            @"EXAMPLE");
4409    }
4410
4411    #[test]
4412    fn test_hyperlink_function_with_separate() {
4413        let env = TestTemplateEnv::new();
4414        // separate() uses FormatRecorder internally; hyperlinks are preserved
4415        insta::assert_snapshot!(
4416            env.render_ok(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
4417            @r"]8;;http://a.com\A]8;;\ | ]8;;http://b.com\B]8;;\");
4418    }
4419
4420    #[test]
4421    fn test_hyperlink_function_with_coalesce() {
4422        let env = TestTemplateEnv::new();
4423        // coalesce() uses FormatRecorder; hyperlinks are preserved
4424        insta::assert_snapshot!(
4425            env.render_ok(r#"coalesce(hyperlink("http://example.com", "Link"), "fallback")"#),
4426            @r"]8;;http://example.com\Link]8;;\");
4427        // Falls back to second when hyperlink text is empty
4428        insta::assert_snapshot!(
4429            env.render_ok(r#"coalesce(hyperlink("http://example.com", ""), "fallback")"#),
4430            @"fallback");
4431    }
4432
4433    #[test]
4434    fn test_hyperlink_function_with_if() {
4435        let env = TestTemplateEnv::new();
4436        // if() does not use FormatRecorder; hyperlinks work directly
4437        insta::assert_snapshot!(
4438            env.render_ok(r#"if(true, hyperlink("http://example.com", "Yes"), "No")"#),
4439            @r"]8;;http://example.com\Yes]8;;\");
4440        insta::assert_snapshot!(
4441            env.render_ok(r#"if(false, "Yes", hyperlink("http://example.com", "No"))"#),
4442            @r"]8;;http://example.com\No]8;;\");
4443    }
4444
4445    #[test]
4446    fn test_hyperlink_function_plain_with_separate() {
4447        let env = TestTemplateEnv::new();
4448        // When rendering plain, hyperlinks should fall back to text
4449        insta::assert_snapshot!(
4450            env.render_plain(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
4451            @"A | B");
4452    }
4453
4454    #[test]
4455    fn test_stringify_function() {
4456        let mut env = TestTemplateEnv::new();
4457        env.add_keyword("none_i64", || literal(None::<i64>));
4458        env.add_color("error", crossterm::style::Color::DarkRed);
4459
4460        insta::assert_snapshot!(env.render_ok("stringify(false)"), @"false");
4461        insta::assert_snapshot!(env.render_ok("stringify(42).len()"), @"2");
4462        insta::assert_snapshot!(env.render_ok("stringify(none_i64)"), @"");
4463        insta::assert_snapshot!(env.render_ok("stringify(label('error', 'text'))"), @"text");
4464    }
4465
4466    #[test]
4467    fn test_json_function() {
4468        let mut env = TestTemplateEnv::new();
4469        env.add_keyword("none_i64", || literal(None::<i64>));
4470        env.add_keyword("string_list", || {
4471            literal(vec!["foo".to_owned(), "bar".to_owned()])
4472        });
4473        env.add_keyword("config_value_table", || {
4474            literal(ConfigValue::from_iter([("foo", "bar")]))
4475        });
4476        env.add_keyword("some_cfgval", || literal(Some(ConfigValue::from(1))));
4477        env.add_keyword("none_cfgval", || literal(None::<ConfigValue>));
4478        env.add_keyword("signature", || {
4479            literal(Signature {
4480                name: "Test User".to_owned(),
4481                email: "test.user@example.com".to_owned(),
4482                timestamp: Timestamp {
4483                    timestamp: MillisSinceEpoch(0),
4484                    tz_offset: 0,
4485                },
4486            })
4487        });
4488        env.add_keyword("email", || literal(Email("foo@bar".to_owned())));
4489        env.add_keyword("size_hint", || literal((5, None)));
4490        env.add_keyword("timestamp", || {
4491            literal(Timestamp {
4492                timestamp: MillisSinceEpoch(0),
4493                tz_offset: 0,
4494            })
4495        });
4496        env.add_keyword("timestamp_range", || {
4497            literal(TimestampRange {
4498                start: Timestamp {
4499                    timestamp: MillisSinceEpoch(0),
4500                    tz_offset: 0,
4501                },
4502                end: Timestamp {
4503                    timestamp: MillisSinceEpoch(86_400_000),
4504                    tz_offset: -60,
4505                },
4506            })
4507        });
4508
4509        insta::assert_snapshot!(env.render_ok(r#"json('"quoted"')"#), @r#""\"quoted\"""#);
4510        insta::assert_snapshot!(env.render_ok(r#"json(string_list)"#), @r#"["foo","bar"]"#);
4511        insta::assert_snapshot!(env.render_ok("json(false)"), @"false");
4512        insta::assert_snapshot!(env.render_ok("json(42)"), @"42");
4513        insta::assert_snapshot!(env.render_ok("json(none_i64)"), @"null");
4514        insta::assert_snapshot!(env.render_ok(r#"json(config_value_table)"#), @r#"{"foo":"bar"}"#);
4515        insta::assert_snapshot!(env.render_ok(r"json(some_cfgval)"), @"1");
4516        insta::assert_snapshot!(env.render_ok(r"json(none_cfgval)"), @"null");
4517        insta::assert_snapshot!(env.render_ok("json(email)"), @r#""foo@bar""#);
4518        insta::assert_snapshot!(
4519            env.render_ok("json(signature)"),
4520            @r#"{"name":"Test User","email":"test.user@example.com","timestamp":"1970-01-01T00:00:00Z"}"#);
4521        insta::assert_snapshot!(env.render_ok("json(size_hint)"), @"[5,null]");
4522        insta::assert_snapshot!(env.render_ok("json(timestamp)"), @r#""1970-01-01T00:00:00Z""#);
4523        insta::assert_snapshot!(
4524            env.render_ok("json(timestamp_range)"),
4525            @r#"{"start":"1970-01-01T00:00:00Z","end":"1970-01-01T23:00:00-01:00"}"#);
4526
4527        // AnyList is serializable if the inner type is.
4528        insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| s))"#), @r#"["foo","bar"]"#);
4529        insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| size_hint))"#), @"[[5,null],[5,null]]");
4530
4531        // Any is serializable if the inner types are.
4532        insta::assert_snapshot!(env.render_ok(r#"json(if(true, email, timestamp))"#), @r#""foo@bar""#);
4533        insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, config_value_table))"#), @"[5,null]");
4534
4535        // The else case missing does prevents the resulting Any expression
4536        // from being serializable.
4537        insta::assert_snapshot!(env.parse_err(r#"json(if(true, email))"#), @r###"
4538         --> 1:6
4539          |
4540        1 | json(if(true, email))
4541          |      ^-------------^
4542          |
4543          = Expected expression of type `Serialize`, but actual type is `Any`
4544        "###);
4545        insta::assert_snapshot!(env.parse_err(r#"json(if(false, email))"#), @r###"
4546         --> 1:6
4547          |
4548        1 | json(if(false, email))
4549          |      ^--------------^
4550          |
4551          = Expected expression of type `Serialize`, but actual type is `Any`
4552        "###);
4553    }
4554
4555    #[test]
4556    fn test_coalesce_function() {
4557        let mut env = TestTemplateEnv::new();
4558        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4559        env.add_keyword("empty_string", || literal("".to_owned()));
4560        env.add_keyword("non_empty_string", || literal("a".to_owned()));
4561
4562        insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
4563        insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
4564        insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
4565        insta::assert_snapshot!(
4566            env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
4567
4568        // "false" is not empty
4569        insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
4570
4571        // Error is not empty
4572        insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
4573        // but can be short-circuited
4574        insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
4575
4576        // Keyword arguments are rejected.
4577        insta::assert_snapshot!(env.parse_err(r#"coalesce("a", value2="b")"#), @r#"
4578         --> 1:15
4579          |
4580        1 | coalesce("a", value2="b")
4581          |               ^--------^
4582          |
4583          = Function `coalesce`: Unexpected keyword arguments
4584        "#);
4585    }
4586
4587    #[test]
4588    fn test_concat_function() {
4589        let mut env = TestTemplateEnv::new();
4590        env.add_keyword("empty", || literal(true));
4591        env.add_keyword("hidden", || literal(false));
4592        env.add_color("empty", crossterm::style::Color::DarkGreen);
4593        env.add_color("error", crossterm::style::Color::DarkRed);
4594        env.add_color("warning", crossterm::style::Color::DarkYellow);
4595
4596        insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
4597        insta::assert_snapshot!(
4598            env.render_ok(r#"concat(hidden, empty)"#),
4599            @"falsetrue");
4600        insta::assert_snapshot!(
4601            env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
4602            @"ab");
4603
4604        // Keyword arguments are rejected.
4605        insta::assert_snapshot!(env.parse_err(r#"concat("a", value2="b")"#), @r#"
4606         --> 1:13
4607          |
4608        1 | concat("a", value2="b")
4609          |             ^--------^
4610          |
4611          = Function `concat`: Unexpected keyword arguments
4612        "#);
4613    }
4614
4615    #[test]
4616    fn test_join_function() {
4617        let mut env = TestTemplateEnv::new();
4618        env.add_keyword("description", || literal("".to_owned()));
4619        env.add_keyword("empty", || literal(true));
4620        env.add_keyword("hidden", || literal(false));
4621        env.add_color("empty", crossterm::style::Color::DarkGreen);
4622        env.add_color("error", crossterm::style::Color::DarkRed);
4623        env.add_color("warning", crossterm::style::Color::DarkYellow);
4624
4625        // Template literals.
4626        insta::assert_snapshot!(env.render_ok(r#"join(",")"#), @"");
4627        insta::assert_snapshot!(env.render_ok(r#"join(",", "")"#), @"");
4628        insta::assert_snapshot!(env.render_ok(r#"join(",", "a")"#), @"a");
4629        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b")"#), @"a,b");
4630        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "", "b")"#), @"a,,b");
4631        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b", "")"#), @"a,b,");
4632        insta::assert_snapshot!(env.render_ok(r#"join(",", "", "a", "b")"#), @",a,b");
4633        insta::assert_snapshot!(
4634            env.render_ok(r#"join("--", 1, "", true, "test", "")"#),
4635            @"1----true--test--");
4636
4637        // Separator is required.
4638        insta::assert_snapshot!(env.parse_err(r#"join()"#), @"
4639         --> 1:6
4640          |
4641        1 | join()
4642          |      ^
4643          |
4644          = Function `join`: Expected at least 1 arguments
4645        ");
4646
4647        // Labeled.
4648        insta::assert_snapshot!(
4649            env.render_ok(r#"join(",", label("error", ""), label("warning", "a"), "b")"#),
4650            @",a,b");
4651        insta::assert_snapshot!(
4652            env.render_ok(
4653                r#"join(label("empty", "<>"), label("error", "a"), label("warning", ""), "b")"#),
4654            @"a<><>b");
4655
4656        // List template.
4657        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ ""))"#), @"a,");
4658        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ "b"))"#), @"a,b");
4659
4660        // Nested.
4661        insta::assert_snapshot!(
4662            env.render_ok(r#"join(",", "a", join("|", "", ""))"#), @"a,|");
4663        insta::assert_snapshot!(
4664            env.render_ok(r#"join(",", "a", join("|", "b", ""))"#), @"a,b|");
4665        insta::assert_snapshot!(
4666            env.render_ok(r#"join(",", "a", join("|", "b", "c"))"#), @"a,b|c");
4667
4668        // Keywords.
4669        insta::assert_snapshot!(
4670            env.render_ok(r#"join(",", hidden, description, empty)"#),
4671            @"false,,true");
4672        insta::assert_snapshot!(
4673            env.render_ok(r#"join(hidden, "X", "Y", "Z")"#),
4674            @"XfalseYfalseZ");
4675        insta::assert_snapshot!(
4676            env.render_ok(r#"join(hidden, empty)"#),
4677            @"true");
4678
4679        // Keyword arguments are rejected.
4680        insta::assert_snapshot!(env.parse_err(r#"join(",", "a", arg="b")"#), @r#"
4681         --> 1:16
4682          |
4683        1 | join(",", "a", arg="b")
4684          |                ^-----^
4685          |
4686          = Function `join`: Unexpected keyword arguments
4687        "#);
4688
4689        // only size hints cannot be templated / joined
4690        env.add_keyword("str_list", || {
4691            literal(vec!["foo".to_owned(), "bar".to_owned()])
4692        });
4693        env.add_keyword("none_int", || literal(None::<i64>));
4694        env.add_keyword("some_int", || literal(Some(67)));
4695        env.add_keyword("cfg_val", || {
4696            literal(ConfigValue::from_iter([("foo", "bar")]))
4697        });
4698        env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
4699        env.add_keyword("signature", || {
4700            literal(new_signature("User", "user@example.com"))
4701        });
4702        env.add_keyword("size_hint", || literal((10, None)));
4703        env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
4704        env.add_keyword("timestamp_range", || {
4705            literal(TimestampRange {
4706                start: new_timestamp(0, 0),
4707                end: new_timestamp(0, 0),
4708            })
4709        });
4710        insta::assert_snapshot!(
4711            env.render_ok("join('|', str_list, 42, none_int, some_int)"),
4712            @"foo bar|42||67");
4713        insta::assert_snapshot!(
4714            env.render_ok("join('|', cfg_val, email, signature, if(true, 42), if(false, 42))"),
4715            @r#"{ foo = "bar" }|me@example.com|User <user@example.com>|42|"#);
4716        insta::assert_snapshot!(
4717            env.render_ok("join('|', timestamp, timestamp_range, str_list.map(|x| x))"),
4718            @"1970-01-01 00:00:00.000 +00:00|1970-01-01 00:00:00.000 +00:00 - 1970-01-01 00:00:00.000 +00:00|foo bar");
4719        assert_matches!(
4720            env.parse_err_kind("join('|', size_hint)"),
4721            TemplateParseErrorKind::Expression(_)
4722        );
4723    }
4724
4725    #[test]
4726    fn test_separate_function() {
4727        let mut env = TestTemplateEnv::new();
4728        env.add_keyword("description", || literal("".to_owned()));
4729        env.add_keyword("empty", || literal(true));
4730        env.add_keyword("hidden", || literal(false));
4731        env.add_color("empty", crossterm::style::Color::DarkGreen);
4732        env.add_color("error", crossterm::style::Color::DarkRed);
4733        env.add_color("warning", crossterm::style::Color::DarkYellow);
4734
4735        insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
4736        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
4737        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
4738        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
4739        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
4740        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
4741        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
4742
4743        // Labeled
4744        insta::assert_snapshot!(
4745            env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
4746            @"a b");
4747
4748        // List template
4749        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
4750        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
4751
4752        // Nested separate
4753        insta::assert_snapshot!(
4754            env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
4755        insta::assert_snapshot!(
4756            env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
4757        insta::assert_snapshot!(
4758            env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
4759
4760        // Conditional template
4761        insta::assert_snapshot!(
4762            env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
4763        insta::assert_snapshot!(
4764            env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
4765        insta::assert_snapshot!(
4766            env.render_ok(r#"separate(" ", "a", if(false, "t"))"#), @"a");
4767        insta::assert_snapshot!(
4768            env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
4769        insta::assert_snapshot!(
4770            env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
4771
4772        // Separate keywords
4773        insta::assert_snapshot!(
4774            env.render_ok(r#"separate(" ", hidden, description, empty)"#),
4775            @"false true");
4776
4777        // Keyword as separator
4778        insta::assert_snapshot!(
4779            env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
4780            @"XfalseYfalseZ");
4781
4782        // Keyword arguments are rejected.
4783        insta::assert_snapshot!(env.parse_err(r#"separate(" ", "a", value2="b")"#), @r#"
4784         --> 1:20
4785          |
4786        1 | separate(" ", "a", value2="b")
4787          |                    ^--------^
4788          |
4789          = Function `separate`: Unexpected keyword arguments
4790        "#);
4791    }
4792
4793    #[test]
4794    fn test_surround_function() {
4795        let mut env = TestTemplateEnv::new();
4796        env.add_keyword("lt", || literal("<".to_owned()));
4797        env.add_keyword("gt", || literal(">".to_owned()));
4798        env.add_keyword("content", || literal("content".to_owned()));
4799        env.add_keyword("empty_content", || literal("".to_owned()));
4800        env.add_color("error", crossterm::style::Color::DarkRed);
4801        env.add_color("paren", crossterm::style::Color::Cyan);
4802
4803        insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
4804        insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
4805
4806        // Labeled
4807        insta::assert_snapshot!(
4808            env.render_ok(
4809                r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
4810            @"(a)");
4811
4812        // Keyword
4813        insta::assert_snapshot!(
4814            env.render_ok(r#"surround(lt, gt, content)"#),
4815            @"<content>");
4816        insta::assert_snapshot!(
4817            env.render_ok(r#"surround(lt, gt, empty_content)"#),
4818            @"");
4819
4820        // Conditional template as content
4821        insta::assert_snapshot!(
4822            env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
4823            @"<empty>");
4824        insta::assert_snapshot!(
4825            env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
4826            @"");
4827    }
4828
4829    #[test]
4830    fn test_config_function() {
4831        use jj_lib::config::ConfigLayer;
4832        use jj_lib::config::ConfigSource;
4833
4834        let mut config = StackedConfig::with_defaults();
4835        config
4836            .add_layer(ConfigLayer::parse(ConfigSource::User, "user.name = 'Test User'").unwrap());
4837        config.add_layer(
4838            ConfigLayer::parse(ConfigSource::User, "user.email = 'test@example.com'").unwrap(),
4839        );
4840
4841        let mut env = TestTemplateEnv::with_config(config);
4842
4843        // valid config path
4844        insta::assert_snapshot!(env.render_ok(r#"config("user.name")"#), @"'Test User'");
4845        insta::assert_snapshot!(env.render_ok(r#"config("user.email")"#), @"'test@example.com'");
4846        insta::assert_snapshot!(env.render_ok(r#"config("user")"#), @"{ email = 'test@example.com', name = 'Test User' }");
4847
4848        // nonexistent config path
4849        insta::assert_snapshot!(env.render_ok(r#"config("non.existent")"#), @"");
4850
4851        // conditional on config path existence
4852        insta::assert_snapshot!(env.render_ok(r#"if(config("user.name"), "yes", "no")"#), @"yes");
4853        insta::assert_snapshot!(env.render_ok(r#"if(config("non.existent"), "yes", "no")"#), @"no");
4854
4855        // malformed config path
4856        insta::assert_snapshot!(env.parse_err("config('user|name')"), @"
4857         --> 1:8
4858          |
4859        1 | config('user|name')
4860          |        ^---------^
4861          |
4862          = Failed to parse config name
4863        TOML parse error at line 1, column 5
4864          |
4865        1 | user|name
4866          |     ^
4867        invalid unquoted key, expected letters, numbers, `-`, `_`
4868        ");
4869
4870        // lookup at parse time
4871        env.add_alias("config_key", r#""name""#);
4872        insta::assert_snapshot!(env.render_ok(r#"config("user." ++ "name")"#), @"'Test User'");
4873        insta::assert_snapshot!(env.render_ok(r#"config("us" ++ "er")"#), @"{ email = 'test@example.com', name = 'Test User' }");
4874        insta::assert_snapshot!(env.render_ok(r#"config("user." ++ config_key)"#), @"'Test User'");
4875
4876        // invalid expression
4877        insta::assert_snapshot!(env.parse_err(r#"config("user." ++)"#), @r#"
4878         --> 1:18
4879          |
4880        1 | config("user." ++)
4881          |                  ^---
4882          |
4883          = expected <expression>
4884        "#);
4885        insta::assert_snapshot!(env.parse_err(r#"config("user|" ++ "name")"#), @r#"
4886         --> 1:8
4887          |
4888        1 | config("user|" ++ "name")
4889          |        ^---------------^
4890          |
4891          = Failed to parse config name
4892        TOML parse error at line 1, column 5
4893          |
4894        1 | user|name
4895          |     ^
4896        invalid unquoted key, expected letters, numbers, `-`, `_`
4897        "#);
4898        insta::assert_snapshot!(env.parse_err(r#"config(invalid)"#), @"
4899         --> 1:8
4900          |
4901        1 | config(invalid)
4902          |        ^-----^
4903          |
4904          = Keyword `invalid` doesn't exist
4905        ");
4906
4907        // dynamic lookup using a keyword that depends on runtime context
4908        env.add_dynamic_keyword("dyn_config_name", || "user.name".to_owned());
4909        insta::assert_snapshot!(
4910            env.render_ok(r#"config(dyn_config_name)"#), @"'Test User'"
4911        );
4912
4913        // dynamic lookup with nonexistent path
4914        env.add_dynamic_keyword("dyn_missing", || "non.existent".to_owned());
4915        insta::assert_snapshot!(env.render_ok(r#"config(dyn_missing)"#), @"");
4916
4917        // dynamic lookup with invalid config path at runtime
4918        env.add_dynamic_keyword("dyn_bad_path", || "user|name".to_owned());
4919        insta::assert_snapshot!(env.render_ok(r#"config(dyn_bad_path)"#), @r"
4920        <Error: TOML parse error at line 1, column 5
4921          |
4922        1 | user|name
4923          |     ^
4924        invalid unquoted key, expected letters, numbers, `-`, `_`
4925        >
4926        ");
4927
4928        // dynamic lookup where name expression itself fails at runtime
4929        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4930        insta::assert_snapshot!(env.render_ok(r#"config(bad_string)"#), @"<Error: Bad>");
4931    }
4932
4933    #[test]
4934    fn test_any_type() {
4935        let mut env = TestTemplateEnv::new();
4936        env.add_keyword("size_hint", || literal((5, None)));
4937        env.add_keyword("size_hint_2", || literal((10, None)));
4938        env.add_keyword("words", || {
4939            literal(vec!["foo".to_owned(), "bar".to_owned()])
4940        });
4941        env.add_color("red", crossterm::style::Color::Red);
4942
4943        // If requires both halves of the statement to support the trait.
4944        insta::assert_snapshot!(env.render_ok(r#"if(true, label("red", "a"), "b")"#), @"a");
4945        insta::assert_snapshot!(env.render_ok(r#"if(false, label("red", "a"), "b")"#), @"b");
4946        insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, size_hint_2))"#), @"[5,null]");
4947        insta::assert_snapshot!(env.render_ok(r#"json(if(false, size_hint, size_hint_2))"#), @"[10,null]");
4948
4949        // If one of the cases does not support Template/Serialize, fail even if
4950        // that case isn't selected.
4951        insta::assert_snapshot!(env.parse_err(r#"if(true, label("red", "a"), size_hint)"#), @r#"
4952         --> 1:1
4953          |
4954        1 | if(true, label("red", "a"), size_hint)
4955          | ^------------------------------------^
4956          |
4957          = Expected expression of type `Template`, but actual type is `Any`
4958        "#);
4959        insta::assert_snapshot!(env.parse_err(r#"json(if(true, size_hint, label("red", "a")))"#), @r#"
4960         --> 1:6
4961          |
4962        1 | json(if(true, size_hint, label("red", "a")))
4963          |      ^------------------------------------^
4964          |
4965          = Expected expression of type `Serialize`, but actual type is `Any`
4966        "#);
4967
4968        // The `join` method should not be available on `Any`.
4969        insta::assert_snapshot!(env.parse_err(r#"if(true,words,words).join(", ")"#), @r#"
4970         --> 1:22
4971          |
4972        1 | if(true,words,words).join(", ")
4973          |                      ^--^
4974          |
4975          = Method `join` doesn't exist for type `Any`
4976        "#);
4977    }
4978
4979    #[test]
4980    fn test_any_list_type() {
4981        let mut env = TestTemplateEnv::new();
4982        env.add_keyword("words", || {
4983            literal(vec!["foo".to_owned(), "bar".to_owned()])
4984        });
4985        env.add_keyword("size_hint", || literal((10, None)));
4986        env.add_color("red", crossterm::style::Color::Red);
4987
4988        // Map items are not required to implement both Template and Serialize.
4989        insta::assert_snapshot!(env.render_ok(
4990            r#"words.map(|x| label("red", x))"#),
4991            @"foo bar");
4992        insta::assert_snapshot!(env.render_ok(
4993            r#"words.map(|x| label("red", x)).join(",")"#),
4994            @"foo,bar");
4995        insta::assert_snapshot!(env.render_ok(
4996            r#"json(words.map(|x| size_hint))"#),
4997            @"[[10,null],[10,null]]");
4998
4999        // You cannot use the result if when the trait is not implemented.
5000        insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint)"#), @r#"
5001         --> 1:1
5002          |
5003        1 | words.map(|x| size_hint)
5004          | ^----------------------^
5005          |
5006          = Expected expression of type `Template`, but actual type is `AnyList`
5007        "#);
5008        insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint).join(",")"#), @r#"
5009         --> 1:26
5010          |
5011        1 | words.map(|x| size_hint).join(",")
5012          |                          ^--^
5013          |
5014          = Expected expression of type `Template`, but actual type is `AnyList`
5015        "#);
5016        insta::assert_snapshot!(env.parse_err(r#"json(words.map(|x| label("red", x)))"#), @r#"
5017         --> 1:6
5018          |
5019        1 | json(words.map(|x| label("red", x)))
5020          |      ^----------------------------^
5021          |
5022          = Expected expression of type `Serialize`, but actual type is `AnyList`
5023        "#);
5024    }
5025}