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_exact_arguments()?;
1084            let start_idx_property =
1085                expect_isize_expression(language, diagnostics, build_ctx, start_idx)?;
1086            let end_idx_property =
1087                expect_isize_expression(language, diagnostics, build_ctx, end_idx)?;
1088            let out_property = (self_property, start_idx_property, end_idx_property).map(
1089                |(s, start_idx, end_idx)| {
1090                    let start_idx = string_index_to_char_boundary(&s, start_idx);
1091                    let end_idx = string_index_to_char_boundary(&s, end_idx);
1092                    s.get(start_idx..end_idx).unwrap_or_default().to_owned()
1093                },
1094            );
1095            Ok(out_property.into_dyn_wrapped())
1096        },
1097    );
1098    map.insert(
1099        "first_line",
1100        |_language, _diagnostics, _build_ctx, self_property, function| {
1101            function.expect_no_arguments()?;
1102            let out_property =
1103                self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
1104            Ok(out_property.into_dyn_wrapped())
1105        },
1106    );
1107    map.insert(
1108        "lines",
1109        |_language, _diagnostics, _build_ctx, self_property, function| {
1110            function.expect_no_arguments()?;
1111            let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect_vec());
1112            Ok(out_property.into_dyn_wrapped())
1113        },
1114    );
1115    map.insert(
1116        "split",
1117        |language, diagnostics, build_ctx, self_property, function| {
1118            let ([separator_node], [limit_node]) = function.expect_arguments()?;
1119            let pattern = template_parser::expect_string_pattern(separator_node)?;
1120            let regex = pattern.to_regex();
1121
1122            if let Some(limit_node) = limit_node {
1123                let limit_property =
1124                    expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1125                let out_property =
1126                    (self_property, limit_property).and_then(move |(haystack, limit)| {
1127                        let haystack_bytes = haystack.as_bytes();
1128                        let parts: Vec<_> = regex
1129                            .splitn(haystack_bytes, limit)
1130                            .map(|part| str::from_utf8(part).map(|s| s.to_owned()))
1131                            .try_collect()?;
1132                        Ok(parts)
1133                    });
1134                Ok(out_property.into_dyn_wrapped())
1135            } else {
1136                let out_property = self_property.and_then(move |haystack| {
1137                    let haystack_bytes = haystack.as_bytes();
1138                    let parts: Vec<_> = regex
1139                        .split(haystack_bytes)
1140                        .map(|part| str::from_utf8(part).map(|s| s.to_owned()))
1141                        .try_collect()?;
1142                    Ok(parts)
1143                });
1144                Ok(out_property.into_dyn_wrapped())
1145            }
1146        },
1147    );
1148    map.insert(
1149        "upper",
1150        |_language, _diagnostics, _build_ctx, self_property, function| {
1151            function.expect_no_arguments()?;
1152            let out_property = self_property.map(|s| s.to_uppercase());
1153            Ok(out_property.into_dyn_wrapped())
1154        },
1155    );
1156    map.insert(
1157        "lower",
1158        |_language, _diagnostics, _build_ctx, self_property, function| {
1159            function.expect_no_arguments()?;
1160            let out_property = self_property.map(|s| s.to_lowercase());
1161            Ok(out_property.into_dyn_wrapped())
1162        },
1163    );
1164    map.insert(
1165        "escape_json",
1166        |_language, _diagnostics, _build_ctx, self_property, function| {
1167            function.expect_no_arguments()?;
1168            let out_property = self_property.map(|s| serde_json::to_string(&s).unwrap());
1169            Ok(out_property.into_dyn_wrapped())
1170        },
1171    );
1172    map.insert(
1173        "replace",
1174        |language, diagnostics, build_ctx, self_property, function| {
1175            let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
1176            let pattern = template_parser::expect_string_pattern(pattern_node)?;
1177            let replacement_property =
1178                expect_stringify_expression(language, diagnostics, build_ctx, replacement_node)?;
1179
1180            let regex = pattern.to_regex();
1181
1182            if let Some(limit_node) = limit_node {
1183                let limit_property =
1184                    expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1185                let out_property = (self_property, replacement_property, limit_property).and_then(
1186                    move |(haystack, replacement, limit)| {
1187                        if limit == 0 {
1188                            // We need to special-case zero because regex.replacen(_, 0, _) replaces
1189                            // all occurrences, and we want zero to mean no occurrences are
1190                            // replaced.
1191                            Ok(haystack)
1192                        } else {
1193                            let haystack_bytes = haystack.as_bytes();
1194                            let replace_bytes = replacement.as_bytes();
1195                            let result = regex.replacen(haystack_bytes, limit, replace_bytes);
1196                            Ok(str::from_utf8(&result)?.to_owned())
1197                        }
1198                    },
1199                );
1200                Ok(out_property.into_dyn_wrapped())
1201            } else {
1202                let out_property = (self_property, replacement_property).and_then(
1203                    move |(haystack, replacement)| {
1204                        let haystack_bytes = haystack.as_bytes();
1205                        let replace_bytes = replacement.as_bytes();
1206                        let result = regex.replace_all(haystack_bytes, replace_bytes);
1207                        Ok(str::from_utf8(&result)?.to_owned())
1208                    },
1209                );
1210                Ok(out_property.into_dyn_wrapped())
1211            }
1212        },
1213    );
1214    map
1215}
1216
1217/// Clamps and aligns the given index `i` to char boundary.
1218///
1219/// Negative index counts from the end. If the index isn't at a char boundary,
1220/// it will be rounded towards 0 (left or right depending on the sign.)
1221fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
1222    // TODO: use floor/ceil_char_boundary() if get stabilized
1223    let magnitude = i.unsigned_abs();
1224    if i < 0 {
1225        let p = s.len().saturating_sub(magnitude);
1226        (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
1227    } else {
1228        let p = magnitude.min(s.len());
1229        (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
1230    }
1231}
1232
1233fn builtin_config_value_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1234-> TemplateBuildMethodFnMap<'a, L, ConfigValue> {
1235    fn extract<'de, T: Deserialize<'de>>(value: ConfigValue) -> Result<T, TemplatePropertyError> {
1236        T::deserialize(value.into_deserializer())
1237            // map to err.message() because TomlError appends newline to it
1238            .map_err(|err| TemplatePropertyError(err.message().into()))
1239    }
1240
1241    // Not using maplit::hashmap!{} or custom declarative macro here because
1242    // code completion inside macro is quite restricted.
1243    let mut map = TemplateBuildMethodFnMap::<L, ConfigValue>::new();
1244    // These methods are called "as_<type>", not "to_<type>" to clarify that
1245    // they'll never convert types (e.g. integer to string.) Since templater
1246    // doesn't provide binding syntax, there's no need to distinguish between
1247    // reference and consuming access.
1248    map.insert(
1249        "as_boolean",
1250        |_language, _diagnostics, _build_ctx, self_property, function| {
1251            function.expect_no_arguments()?;
1252            let out_property = self_property.and_then(extract::<bool>);
1253            Ok(out_property.into_dyn_wrapped())
1254        },
1255    );
1256    map.insert(
1257        "as_integer",
1258        |_language, _diagnostics, _build_ctx, self_property, function| {
1259            function.expect_no_arguments()?;
1260            let out_property = self_property.and_then(extract::<i64>);
1261            Ok(out_property.into_dyn_wrapped())
1262        },
1263    );
1264    map.insert(
1265        "as_string",
1266        |_language, _diagnostics, _build_ctx, self_property, function| {
1267            function.expect_no_arguments()?;
1268            let out_property = self_property.and_then(extract::<String>);
1269            Ok(out_property.into_dyn_wrapped())
1270        },
1271    );
1272    map.insert(
1273        "as_string_list",
1274        |_language, _diagnostics, _build_ctx, self_property, function| {
1275            function.expect_no_arguments()?;
1276            let out_property = self_property.and_then(extract::<Vec<String>>);
1277            Ok(out_property.into_dyn_wrapped())
1278        },
1279    );
1280    // TODO: add is_<type>() -> Boolean?
1281    // TODO: add .get(key) -> ConfigValue or Option<ConfigValue>?
1282    map
1283}
1284
1285fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1286-> TemplateBuildMethodFnMap<'a, L, Signature> {
1287    // Not using maplit::hashmap!{} or custom declarative macro here because
1288    // code completion inside macro is quite restricted.
1289    let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
1290    map.insert(
1291        "name",
1292        |_language, _diagnostics, _build_ctx, self_property, function| {
1293            function.expect_no_arguments()?;
1294            let out_property = self_property.map(|signature| signature.name);
1295            Ok(out_property.into_dyn_wrapped())
1296        },
1297    );
1298    map.insert(
1299        "email",
1300        |_language, _diagnostics, _build_ctx, self_property, function| {
1301            function.expect_no_arguments()?;
1302            let out_property = self_property.map(|signature| Email(signature.email));
1303            Ok(out_property.into_dyn_wrapped())
1304        },
1305    );
1306    map.insert(
1307        "timestamp",
1308        |_language, _diagnostics, _build_ctx, self_property, function| {
1309            function.expect_no_arguments()?;
1310            let out_property = self_property.map(|signature| signature.timestamp);
1311            Ok(out_property.into_dyn_wrapped())
1312        },
1313    );
1314    map
1315}
1316
1317fn builtin_email_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1318-> TemplateBuildMethodFnMap<'a, L, Email> {
1319    // Not using maplit::hashmap!{} or custom declarative macro here because
1320    // code completion inside macro is quite restricted.
1321    let mut map = TemplateBuildMethodFnMap::<L, Email>::new();
1322    map.insert(
1323        "local",
1324        |_language, _diagnostics, _build_ctx, self_property, function| {
1325            function.expect_no_arguments()?;
1326            let out_property = self_property.map(|email| {
1327                let (local, _) = text_util::split_email(&email.0);
1328                local.to_owned()
1329            });
1330            Ok(out_property.into_dyn_wrapped())
1331        },
1332    );
1333    map.insert(
1334        "domain",
1335        |_language, _diagnostics, _build_ctx, self_property, function| {
1336            function.expect_no_arguments()?;
1337            let out_property = self_property.map(|email| {
1338                let (_, domain) = text_util::split_email(&email.0);
1339                domain.unwrap_or_default().to_owned()
1340            });
1341            Ok(out_property.into_dyn_wrapped())
1342        },
1343    );
1344    map
1345}
1346
1347fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1348-> TemplateBuildMethodFnMap<'a, L, SizeHint> {
1349    // Not using maplit::hashmap!{} or custom declarative macro here because
1350    // code completion inside macro is quite restricted.
1351    let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
1352    map.insert(
1353        "lower",
1354        |_language, _diagnostics, _build_ctx, self_property, function| {
1355            function.expect_no_arguments()?;
1356            let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
1357            Ok(out_property.into_dyn_wrapped())
1358        },
1359    );
1360    map.insert(
1361        "upper",
1362        |_language, _diagnostics, _build_ctx, self_property, function| {
1363            function.expect_no_arguments()?;
1364            let out_property =
1365                self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
1366            Ok(out_property.into_dyn_wrapped())
1367        },
1368    );
1369    map.insert(
1370        "exact",
1371        |_language, _diagnostics, _build_ctx, self_property, function| {
1372            function.expect_no_arguments()?;
1373            let out_property = self_property.and_then(|(lower, upper)| {
1374                let exact = (Some(lower) == upper).then_some(lower);
1375                Ok(exact.map(i64::try_from).transpose()?)
1376            });
1377            Ok(out_property.into_dyn_wrapped())
1378        },
1379    );
1380    map.insert(
1381        "zero",
1382        |_language, _diagnostics, _build_ctx, self_property, function| {
1383            function.expect_no_arguments()?;
1384            let out_property = self_property.map(|(_, upper)| upper == Some(0));
1385            Ok(out_property.into_dyn_wrapped())
1386        },
1387    );
1388    map
1389}
1390
1391fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1392-> TemplateBuildMethodFnMap<'a, L, Timestamp> {
1393    // Not using maplit::hashmap!{} or custom declarative macro here because
1394    // code completion inside macro is quite restricted.
1395    let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
1396    map.insert(
1397        "ago",
1398        |_language, _diagnostics, _build_ctx, self_property, function| {
1399            function.expect_no_arguments()?;
1400            let now = Timestamp::now();
1401            let format = timeago::Formatter::new();
1402            let out_property = self_property.and_then(move |timestamp| {
1403                Ok(time_util::format_duration(&timestamp, &now, &format)?)
1404            });
1405            Ok(out_property.into_dyn_wrapped())
1406        },
1407    );
1408    map.insert(
1409        "format",
1410        |_language, diagnostics, _build_ctx, self_property, function| {
1411            // No dynamic string is allowed as the templater has no runtime error type.
1412            let [format_node] = function.expect_exact_arguments()?;
1413            let format =
1414                template_parser::catch_aliases(diagnostics, format_node, |_diagnostics, node| {
1415                    let format = template_parser::expect_string_literal(node)?;
1416                    time_util::FormattingItems::parse(format).ok_or_else(|| {
1417                        TemplateParseError::expression("Invalid time format", node.span)
1418                    })
1419                })?
1420                .into_owned();
1421            let out_property = self_property.and_then(move |timestamp| {
1422                Ok(time_util::format_absolute_timestamp_with(
1423                    &timestamp, &format,
1424                )?)
1425            });
1426            Ok(out_property.into_dyn_wrapped())
1427        },
1428    );
1429    map.insert(
1430        "utc",
1431        |_language, _diagnostics, _build_ctx, self_property, function| {
1432            function.expect_no_arguments()?;
1433            let out_property = self_property.map(|mut timestamp| {
1434                timestamp.tz_offset = 0;
1435                timestamp
1436            });
1437            Ok(out_property.into_dyn_wrapped())
1438        },
1439    );
1440    map.insert(
1441        "local",
1442        |_language, _diagnostics, _build_ctx, self_property, function| {
1443            function.expect_no_arguments()?;
1444            let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
1445                .ok()
1446                .and_then(|tz_string| tz_string.parse::<i32>().ok())
1447                .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
1448            let out_property = self_property.map(move |mut timestamp| {
1449                timestamp.tz_offset = tz_offset;
1450                timestamp
1451            });
1452            Ok(out_property.into_dyn_wrapped())
1453        },
1454    );
1455    map.insert(
1456        "after",
1457        |_language, diagnostics, _build_ctx, self_property, function| {
1458            let [date_pattern_node] = function.expect_exact_arguments()?;
1459            let now = chrono::Local::now();
1460            let date_pattern = template_parser::catch_aliases(
1461                diagnostics,
1462                date_pattern_node,
1463                |_diagnostics, node| {
1464                    let date_pattern = template_parser::expect_string_literal(node)?;
1465                    DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
1466                        TemplateParseError::expression("Invalid date pattern", node.span)
1467                            .with_source(err)
1468                    })
1469                },
1470            )?;
1471            let out_property = self_property.map(move |timestamp| date_pattern.matches(&timestamp));
1472            Ok(out_property.into_dyn_wrapped())
1473        },
1474    );
1475    map.insert("before", map["after"]);
1476    map.insert(
1477        "since",
1478        |language, diagnostics, build_ctx, self_property, function| {
1479            let [date_node] = function.expect_exact_arguments()?;
1480            let date_property =
1481                expect_timestamp_expression(language, diagnostics, build_ctx, date_node)?;
1482            let out_property =
1483                (self_property, date_property).and_then(move |(self_timestamp, arg_timestamp)| {
1484                    Ok(TimestampRange {
1485                        start: arg_timestamp,
1486                        end: self_timestamp,
1487                    })
1488                });
1489            Ok(out_property.into_dyn_wrapped())
1490        },
1491    );
1492    map
1493}
1494
1495fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1496-> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
1497    // Not using maplit::hashmap!{} or custom declarative macro here because
1498    // code completion inside macro is quite restricted.
1499    let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
1500    map.insert(
1501        "start",
1502        |_language, _diagnostics, _build_ctx, self_property, function| {
1503            function.expect_no_arguments()?;
1504            let out_property = self_property.map(|time_range| time_range.start);
1505            Ok(out_property.into_dyn_wrapped())
1506        },
1507    );
1508    map.insert(
1509        "end",
1510        |_language, _diagnostics, _build_ctx, self_property, function| {
1511            function.expect_no_arguments()?;
1512            let out_property = self_property.map(|time_range| time_range.end);
1513            Ok(out_property.into_dyn_wrapped())
1514        },
1515    );
1516    map.insert(
1517        "duration",
1518        |_language, _diagnostics, _build_ctx, self_property, function| {
1519            function.expect_no_arguments()?;
1520            // TODO: Introduce duration type, and move formatting to it.
1521            let out_property = self_property.and_then(|time_range| {
1522                let mut f = timeago::Formatter::new();
1523                f.min_unit(timeago::TimeUnit::Microseconds).ago("");
1524                let duration = time_util::format_duration(&time_range.start, &time_range.end, &f)?;
1525                if duration == "now" {
1526                    Ok("less than a microsecond".to_owned())
1527                } else {
1528                    Ok(duration)
1529                }
1530            });
1531            Ok(out_property.into_dyn_wrapped())
1532        },
1533    );
1534    map
1535}
1536
1537fn builtin_any_list_methods<'a, L: TemplateLanguage<'a> + ?Sized>() -> BuildAnyMethodFnMap<'a, L> {
1538    // Not using maplit::hashmap!{} or custom declarative macro here because
1539    // code completion inside macro is quite restricted.
1540    let mut map = BuildAnyMethodFnMap::<L>::new();
1541    map.insert(
1542        "join",
1543        |language, diagnostics, build_ctx, self_template, function| {
1544            let [separator_node] = function.expect_exact_arguments()?;
1545            let separator =
1546                expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1547            Ok(L::Property::wrap_template(
1548                self_template.try_join(separator).ok_or_else(|| {
1549                    // FIXME: This error should probably be reported on the type
1550                    // within the AnyListTemplateProperty.
1551                    TemplateParseError::expected_type("Template", "AnyList", function.name_span)
1552                })?,
1553            ))
1554        },
1555    );
1556    map
1557}
1558
1559/// Creates new symbol table for printable list property.
1560pub fn builtin_formattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1561where
1562    L: TemplateLanguage<'a> + ?Sized,
1563    L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1564    O: Template + Clone + 'a,
1565{
1566    let mut map = builtin_unformattable_list_methods::<L, O>();
1567    map.insert(
1568        "join",
1569        |language, diagnostics, build_ctx, self_property, function| {
1570            let [separator_node] = function.expect_exact_arguments()?;
1571            let separator =
1572                expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1573            let template =
1574                ListPropertyTemplate::new(self_property, separator, |formatter, item| {
1575                    item.format(formatter)
1576                });
1577            Ok(L::Property::wrap_template(Box::new(template)))
1578        },
1579    );
1580    map
1581}
1582
1583/// Creates new symbol table for unprintable list property.
1584pub fn builtin_unformattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1585where
1586    L: TemplateLanguage<'a> + ?Sized,
1587    L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1588    O: Clone + 'a,
1589{
1590    // Not using maplit::hashmap!{} or custom declarative macro here because
1591    // code completion inside macro is quite restricted.
1592    let mut map = TemplateBuildMethodFnMap::<L, Vec<O>>::new();
1593    map.insert(
1594        "len",
1595        |_language, _diagnostics, _build_ctx, self_property, function| {
1596            function.expect_no_arguments()?;
1597            let out_property = self_property.and_then(|items| Ok(i64::try_from(items.len())?));
1598            Ok(out_property.into_dyn_wrapped())
1599        },
1600    );
1601    map.insert(
1602        "filter",
1603        |language, diagnostics, build_ctx, self_property, function| {
1604            let out_property: BoxedTemplateProperty<'a, Vec<O>> =
1605                build_filter_operation(language, diagnostics, build_ctx, self_property, function)?;
1606            Ok(L::Property::wrap_property(out_property))
1607        },
1608    );
1609    map.insert(
1610        "map",
1611        |language, diagnostics, build_ctx, self_property, function| {
1612            let map_result =
1613                build_map_operation(language, diagnostics, build_ctx, self_property, function)?;
1614            Ok(L::Property::wrap_any_list(map_result))
1615        },
1616    );
1617    map.insert(
1618        "any",
1619        |language, diagnostics, build_ctx, self_property, function| {
1620            let out_property =
1621                build_any_operation(language, diagnostics, build_ctx, self_property, function)?;
1622            Ok(out_property.into_dyn_wrapped())
1623        },
1624    );
1625    map.insert(
1626        "all",
1627        |language, diagnostics, build_ctx, self_property, function| {
1628            let out_property =
1629                build_all_operation(language, diagnostics, build_ctx, self_property, function)?;
1630            Ok(out_property.into_dyn_wrapped())
1631        },
1632    );
1633    map.insert(
1634        "first",
1635        |_language, _diagnostics, _build_ctx, self_property, function| {
1636            function.expect_no_arguments()?;
1637            // TODO: Return `Option<T>` instead of erroring out.
1638            let out_property = self_property.and_then(|items| {
1639                items
1640                    .into_iter()
1641                    .next()
1642                    .ok_or_else(|| TemplatePropertyError("List is empty".into()))
1643            });
1644            Ok(L::Property::wrap_property(out_property.into_dyn()))
1645        },
1646    );
1647    map.insert(
1648        "last",
1649        |_language, _diagnostics, _build_ctx, self_property, function| {
1650            function.expect_no_arguments()?;
1651            // TODO: Return `Option<T>` instead of erroring out.
1652            let out_property = self_property.and_then(|mut items| {
1653                items
1654                    .pop()
1655                    .ok_or_else(|| TemplatePropertyError("List is empty".into()))
1656            });
1657            Ok(L::Property::wrap_property(out_property.into_dyn()))
1658        },
1659    );
1660    map.insert(
1661        "get",
1662        |language, diagnostics, build_ctx, self_property, function| {
1663            let [index_node] = function.expect_exact_arguments()?;
1664            let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
1665            // TODO: Return `Option<T>` instead of erroring out.
1666            let out_property = (self_property, index).and_then(|(mut items, index)| {
1667                if index < items.len() {
1668                    Ok(items.remove(index))
1669                } else {
1670                    Err(TemplatePropertyError(
1671                        format!("Index {index} out of bounds").into(),
1672                    ))
1673                }
1674            });
1675            Ok(L::Property::wrap_property(out_property.into_dyn()))
1676        },
1677    );
1678    map.insert(
1679        "reverse",
1680        |_language, _diagnostics, _build_ctx, self_property, function| {
1681            function.expect_no_arguments()?;
1682            let out_property = self_property.map(|mut items| {
1683                items.reverse();
1684                items
1685            });
1686            Ok(L::Property::wrap_property(out_property.into_dyn()))
1687        },
1688    );
1689    map.insert(
1690        "skip",
1691        |language, diagnostics, build_ctx, self_property, function| {
1692            let [count_node] = function.expect_exact_arguments()?;
1693            let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
1694            let out_property = (self_property, count)
1695                .map(|(items, count)| items.into_iter().skip(count).collect_vec());
1696            Ok(L::Property::wrap_property(out_property.into_dyn()))
1697        },
1698    );
1699    map.insert(
1700        "take",
1701        |language, diagnostics, build_ctx, self_property, function| {
1702            let [count_node] = function.expect_exact_arguments()?;
1703            let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
1704            let out_property = (self_property, count)
1705                .map(|(items, count)| items.into_iter().take(count).collect_vec());
1706            Ok(L::Property::wrap_property(out_property.into_dyn()))
1707        },
1708    );
1709    map
1710}
1711
1712/// Builds expression that extracts iterable property and filters its items.
1713fn build_filter_operation<'a, L, O, P, B>(
1714    language: &L,
1715    diagnostics: &mut TemplateDiagnostics,
1716    build_ctx: &BuildContext<L::Property>,
1717    self_property: P,
1718    function: &FunctionCallNode,
1719) -> TemplateParseResult<BoxedTemplateProperty<'a, B>>
1720where
1721    L: TemplateLanguage<'a> + ?Sized,
1722    L::Property: WrapTemplateProperty<'a, O>,
1723    P: TemplateProperty + 'a,
1724    P::Output: IntoIterator<Item = O>,
1725    O: Clone + 'a,
1726    B: FromIterator<O>,
1727{
1728    let [lambda_node] = function.expect_exact_arguments()?;
1729    let item_placeholder = PropertyPlaceholder::new();
1730    let item_predicate =
1731        template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1732            let lambda = template_parser::expect_lambda(node)?;
1733            build_lambda_expression(
1734                build_ctx,
1735                lambda,
1736                &[&|| item_placeholder.clone().into_dyn_wrapped()],
1737                |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1738            )
1739        })?;
1740    let out_property = self_property.and_then(move |items| {
1741        items
1742            .into_iter()
1743            .filter_map(|item| {
1744                // Evaluate predicate with the current item
1745                item_placeholder.set(item);
1746                let result = item_predicate.extract();
1747                let item = item_placeholder.take().unwrap();
1748                result.map(|pred| pred.then_some(item)).transpose()
1749            })
1750            .collect()
1751    });
1752    Ok(out_property.into_dyn())
1753}
1754
1755/// Builds expression that extracts iterable property and applies template to
1756/// each item.
1757fn build_map_operation<'a, L, O, P>(
1758    language: &L,
1759    diagnostics: &mut TemplateDiagnostics,
1760    build_ctx: &BuildContext<L::Property>,
1761    self_property: P,
1762    function: &FunctionCallNode,
1763) -> TemplateParseResult<BoxedAnyProperty<'a>>
1764where
1765    L: TemplateLanguage<'a> + ?Sized,
1766    L::Property: WrapTemplateProperty<'a, O>,
1767    P: TemplateProperty + 'a,
1768    P::Output: IntoIterator<Item = O>,
1769    O: Clone + 'a,
1770{
1771    let [lambda_node] = function.expect_exact_arguments()?;
1772    let item_placeholder = PropertyPlaceholder::new();
1773    let mapped_item =
1774        template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1775            let lambda = template_parser::expect_lambda(node)?;
1776            build_lambda_expression(
1777                build_ctx,
1778                lambda,
1779                &[&|| item_placeholder.clone().into_dyn_wrapped()],
1780                |build_ctx, body| expect_any_expression(language, diagnostics, build_ctx, body),
1781            )
1782        })?;
1783    let mapped_list = ListMapProperty::new(self_property, item_placeholder, mapped_item);
1784    Ok(Box::new(mapped_list))
1785}
1786
1787/// Builds expression that checks if any item in the list satisfies the
1788/// predicate.
1789fn build_any_operation<'a, L, O, P>(
1790    language: &L,
1791    diagnostics: &mut TemplateDiagnostics,
1792    build_ctx: &BuildContext<L::Property>,
1793    self_property: P,
1794    function: &FunctionCallNode,
1795) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
1796where
1797    L: TemplateLanguage<'a> + ?Sized,
1798    L::Property: WrapTemplateProperty<'a, O>,
1799    P: TemplateProperty + 'a,
1800    P::Output: IntoIterator<Item = O>,
1801    O: Clone + 'a,
1802{
1803    let [lambda_node] = function.expect_exact_arguments()?;
1804    let item_placeholder = PropertyPlaceholder::new();
1805    let item_predicate =
1806        template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1807            let lambda = template_parser::expect_lambda(node)?;
1808            build_lambda_expression(
1809                build_ctx,
1810                lambda,
1811                &[&|| item_placeholder.clone().into_dyn_wrapped()],
1812                |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1813            )
1814        })?;
1815
1816    let out_property = self_property.and_then(move |items| {
1817        items
1818            .into_iter()
1819            .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
1820            .process_results(|mut predicates| predicates.any(|p| p))
1821    });
1822    Ok(out_property.into_dyn())
1823}
1824
1825/// Builds expression that checks if all items in the list satisfy the
1826/// predicate.
1827fn build_all_operation<'a, L, O, P>(
1828    language: &L,
1829    diagnostics: &mut TemplateDiagnostics,
1830    build_ctx: &BuildContext<L::Property>,
1831    self_property: P,
1832    function: &FunctionCallNode,
1833) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
1834where
1835    L: TemplateLanguage<'a> + ?Sized,
1836    L::Property: WrapTemplateProperty<'a, O>,
1837    P: TemplateProperty + 'a,
1838    P::Output: IntoIterator<Item = O>,
1839    O: Clone + 'a,
1840{
1841    let [lambda_node] = function.expect_exact_arguments()?;
1842    let item_placeholder = PropertyPlaceholder::new();
1843    let item_predicate =
1844        template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1845            let lambda = template_parser::expect_lambda(node)?;
1846            build_lambda_expression(
1847                build_ctx,
1848                lambda,
1849                &[&|| item_placeholder.clone().into_dyn_wrapped()],
1850                |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1851            )
1852        })?;
1853
1854    let out_property = self_property.and_then(move |items| {
1855        items
1856            .into_iter()
1857            .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
1858            .process_results(|mut predicates| predicates.all(|p| p))
1859    });
1860    Ok(out_property.into_dyn())
1861}
1862
1863/// Builds lambda expression to be evaluated with the provided arguments.
1864/// `arg_fns` is usually an array of wrapped [`PropertyPlaceholder`]s.
1865fn build_lambda_expression<'i, P, T>(
1866    build_ctx: &BuildContext<'i, P>,
1867    lambda: &LambdaNode<'i>,
1868    arg_fns: &[&'i dyn Fn() -> P],
1869    build_body: impl FnOnce(&BuildContext<'i, P>, &ExpressionNode<'i>) -> TemplateParseResult<T>,
1870) -> TemplateParseResult<T> {
1871    if lambda.params.len() != arg_fns.len() {
1872        return Err(TemplateParseError::expression(
1873            format!("Expected {} lambda parameters", arg_fns.len()),
1874            lambda.params_span,
1875        ));
1876    }
1877    let mut local_variables = build_ctx.local_variables.clone();
1878    local_variables.extend(iter::zip(&lambda.params, arg_fns));
1879    let inner_build_ctx = BuildContext {
1880        local_variables,
1881        self_variable: build_ctx.self_variable,
1882    };
1883    build_body(&inner_build_ctx, &lambda.body)
1884}
1885
1886fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
1887    // Not using maplit::hashmap!{} or custom declarative macro here because
1888    // code completion inside macro is quite restricted.
1889    let mut map = TemplateBuildFunctionFnMap::<L>::new();
1890    map.insert("fill", |language, diagnostics, build_ctx, function| {
1891        let [width_node, content_node] = function.expect_exact_arguments()?;
1892        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1893        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1894        let template =
1895            ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
1896                Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
1897                Err(err) => formatter.handle_error(err),
1898            });
1899        Ok(L::Property::wrap_template(Box::new(template)))
1900    });
1901    map.insert("indent", |language, diagnostics, build_ctx, function| {
1902        let [prefix_node, content_node] = function.expect_exact_arguments()?;
1903        let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1904        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1905        let template = ReformatTemplate::new(content, move |formatter, recorded| {
1906            let rewrap = formatter.rewrap_fn();
1907            text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
1908                prefix.format(&mut rewrap(formatter))
1909            })
1910        });
1911        Ok(L::Property::wrap_template(Box::new(template)))
1912    });
1913    map.insert("pad_start", |language, diagnostics, build_ctx, function| {
1914        let ([width_node, content_node], [fill_char_node]) =
1915            function.expect_named_arguments(&["", "", "fill_char"])?;
1916        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1917        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1918        let fill_char = fill_char_node
1919            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1920            .transpose()?;
1921        let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
1922        Ok(L::Property::wrap_template(template))
1923    });
1924    map.insert("pad_end", |language, diagnostics, build_ctx, function| {
1925        let ([width_node, content_node], [fill_char_node]) =
1926            function.expect_named_arguments(&["", "", "fill_char"])?;
1927        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1928        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1929        let fill_char = fill_char_node
1930            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1931            .transpose()?;
1932        let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
1933        Ok(L::Property::wrap_template(template))
1934    });
1935    map.insert(
1936        "pad_centered",
1937        |language, diagnostics, build_ctx, function| {
1938            let ([width_node, content_node], [fill_char_node]) =
1939                function.expect_named_arguments(&["", "", "fill_char"])?;
1940            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1941            let content =
1942                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1943            let fill_char = fill_char_node
1944                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1945                .transpose()?;
1946            let template =
1947                new_pad_template(content, fill_char, width, text_util::write_padded_centered);
1948            Ok(L::Property::wrap_template(template))
1949        },
1950    );
1951    map.insert(
1952        "truncate_start",
1953        |language, diagnostics, build_ctx, function| {
1954            let ([width_node, content_node], [ellipsis_node]) =
1955                function.expect_named_arguments(&["", "", "ellipsis"])?;
1956            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1957            let content =
1958                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1959            let ellipsis = ellipsis_node
1960                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1961                .transpose()?;
1962            let template =
1963                new_truncate_template(content, ellipsis, width, text_util::write_truncated_start);
1964            Ok(L::Property::wrap_template(template))
1965        },
1966    );
1967    map.insert(
1968        "truncate_end",
1969        |language, diagnostics, build_ctx, function| {
1970            let ([width_node, content_node], [ellipsis_node]) =
1971                function.expect_named_arguments(&["", "", "ellipsis"])?;
1972            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1973            let content =
1974                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1975            let ellipsis = ellipsis_node
1976                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1977                .transpose()?;
1978            let template =
1979                new_truncate_template(content, ellipsis, width, text_util::write_truncated_end);
1980            Ok(L::Property::wrap_template(template))
1981        },
1982    );
1983    map.insert("hash", |language, diagnostics, build_ctx, function| {
1984        let [content_node] = function.expect_exact_arguments()?;
1985        let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
1986        let result = content.map(|c| hex_util::encode_hex(blake2b_hash(&c).as_ref()));
1987        Ok(result.into_dyn_wrapped())
1988    });
1989    map.insert("label", |language, diagnostics, build_ctx, function| {
1990        let [label_node, content_node] = function.expect_exact_arguments()?;
1991        let label_property =
1992            expect_stringify_expression(language, diagnostics, build_ctx, label_node)?;
1993        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1994        let labels =
1995            label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
1996        Ok(L::Property::wrap_template(Box::new(LabelTemplate::new(
1997            content, labels,
1998        ))))
1999    });
2000    map.insert(
2001        "raw_escape_sequence",
2002        |language, diagnostics, build_ctx, function| {
2003            let [content_node] = function.expect_exact_arguments()?;
2004            let content =
2005                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2006            Ok(L::Property::wrap_template(Box::new(
2007                RawEscapeSequenceTemplate(content),
2008            )))
2009        },
2010    );
2011    map.insert("hyperlink", |language, diagnostics, build_ctx, function| {
2012        let ([url_node, text_node], [fallback_node]) = function.expect_arguments()?;
2013        let url = expect_stringify_expression(language, diagnostics, build_ctx, url_node)?;
2014        let text = expect_template_expression(language, diagnostics, build_ctx, text_node)?;
2015        let fallback = fallback_node
2016            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2017            .transpose()?;
2018        Ok(L::Property::wrap_template(Box::new(
2019            HyperlinkTemplate::new(url, text, fallback),
2020        )))
2021    });
2022    map.insert("stringify", |language, diagnostics, build_ctx, function| {
2023        let [content_node] = function.expect_exact_arguments()?;
2024        let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
2025        Ok(L::Property::wrap_property(content))
2026    });
2027    map.insert("json", |language, diagnostics, build_ctx, function| {
2028        // TODO: Add pretty=true|false? or json(key=value, ..)? The latter might
2029        // be implemented as a map constructor/literal if we add support for
2030        // heterogeneous list/map types.
2031        let [value_node] = function.expect_exact_arguments()?;
2032        let value = expect_serialize_expression(language, diagnostics, build_ctx, value_node)?;
2033        let out_property = value.and_then(|v| Ok(serde_json::to_string(&v)?));
2034        Ok(out_property.into_dyn_wrapped())
2035    });
2036    map.insert("if", |language, diagnostics, build_ctx, function| {
2037        let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
2038        let condition =
2039            expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
2040        let true_any = expect_any_expression(language, diagnostics, build_ctx, true_node)?;
2041        let false_any = false_node
2042            .map(|node| expect_any_expression(language, diagnostics, build_ctx, node))
2043            .transpose()?;
2044        let property = ConditionalProperty::new(condition, true_any, false_any);
2045        Ok(L::Property::wrap_any(Box::new(property)))
2046    });
2047    map.insert("coalesce", |language, diagnostics, build_ctx, function| {
2048        let ([], content_nodes) = function.expect_some_arguments()?;
2049        let contents = content_nodes
2050            .iter()
2051            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2052            .try_collect()?;
2053        Ok(L::Property::wrap_template(Box::new(CoalesceTemplate(
2054            contents,
2055        ))))
2056    });
2057    map.insert("concat", |language, diagnostics, build_ctx, function| {
2058        let ([], content_nodes) = function.expect_some_arguments()?;
2059        let contents = content_nodes
2060            .iter()
2061            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2062            .try_collect()?;
2063        Ok(L::Property::wrap_template(Box::new(ConcatTemplate(
2064            contents,
2065        ))))
2066    });
2067    map.insert("join", |language, diagnostics, build_ctx, function| {
2068        let ([separator_node], content_nodes) = function.expect_some_arguments()?;
2069        let separator =
2070            expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
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(JoinTemplate::new(
2076            separator, contents,
2077        ))))
2078    });
2079    map.insert("separate", |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(SeparateTemplate::new(
2088            separator, contents,
2089        ))))
2090    });
2091    map.insert("surround", |language, diagnostics, build_ctx, function| {
2092        let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
2093        let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
2094        let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
2095        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2096        let template = ReformatTemplate::new(content, move |formatter, recorded| {
2097            if recorded.data().is_empty() {
2098                return Ok(());
2099            }
2100            prefix.format(formatter)?;
2101            recorded.replay(formatter.as_mut())?;
2102            suffix.format(formatter)?;
2103            Ok(())
2104        });
2105        Ok(L::Property::wrap_template(Box::new(template)))
2106    });
2107    map.insert("config", |language, diagnostics, _build_ctx, function| {
2108        // Dynamic lookup can be implemented if needed. The name is literal
2109        // string for now so the error can be reported early.
2110        let [name_node] = function.expect_exact_arguments()?;
2111        let name: ConfigNamePathBuf =
2112            template_parser::catch_aliases(diagnostics, name_node, |_diagnostics, node| {
2113                let name = template_parser::expect_string_literal(node)?;
2114                name.parse().map_err(|err| {
2115                    TemplateParseError::expression("Failed to parse config name", node.span)
2116                        .with_source(err)
2117                })
2118            })?;
2119        let value = language
2120            .settings()
2121            .get_value(&name)
2122            .optional()
2123            .map_err(|err| {
2124                TemplateParseError::expression("Failed to get config value", function.name_span)
2125                    .with_source(err)
2126            })?;
2127        // .decorated("", "") to trim leading/trailing whitespace
2128        Ok(Literal(value.map(|v| v.decorated("", ""))).into_dyn_wrapped())
2129    });
2130    map
2131}
2132
2133fn new_pad_template<'a, W>(
2134    content: Box<dyn Template + 'a>,
2135    fill_char: Option<Box<dyn Template + 'a>>,
2136    width: BoxedTemplateProperty<'a, usize>,
2137    write_padded: W,
2138) -> Box<dyn Template + 'a>
2139where
2140    W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
2141{
2142    let default_fill_char = FormatRecorder::with_data(" ");
2143    let template = ReformatTemplate::new(content, move |formatter, recorded| {
2144        let width = match width.extract() {
2145            Ok(width) => width,
2146            Err(err) => return formatter.handle_error(err),
2147        };
2148        let mut fill_char_recorder;
2149        let recorded_fill_char = if let Some(fill_char) = &fill_char {
2150            let rewrap = formatter.rewrap_fn();
2151            fill_char_recorder = FormatRecorder::new(formatter.maybe_color());
2152            fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
2153            &fill_char_recorder
2154        } else {
2155            &default_fill_char
2156        };
2157        write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
2158    });
2159    Box::new(template)
2160}
2161
2162fn new_truncate_template<'a, W>(
2163    content: Box<dyn Template + 'a>,
2164    ellipsis: Option<Box<dyn Template + 'a>>,
2165    width: BoxedTemplateProperty<'a, usize>,
2166    write_truncated: W,
2167) -> Box<dyn Template + 'a>
2168where
2169    W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<usize> + 'a,
2170{
2171    let default_ellipsis = FormatRecorder::with_data("");
2172    let template = ReformatTemplate::new(content, move |formatter, recorded| {
2173        let width = match width.extract() {
2174            Ok(width) => width,
2175            Err(err) => return formatter.handle_error(err),
2176        };
2177        let mut ellipsis_recorder;
2178        let recorded_ellipsis = if let Some(ellipsis) = &ellipsis {
2179            let rewrap = formatter.rewrap_fn();
2180            ellipsis_recorder = FormatRecorder::new(formatter.maybe_color());
2181            ellipsis.format(&mut rewrap(&mut ellipsis_recorder))?;
2182            &ellipsis_recorder
2183        } else {
2184            &default_ellipsis
2185        };
2186        write_truncated(formatter.as_mut(), recorded, recorded_ellipsis, width)?;
2187        Ok(())
2188    });
2189    Box::new(template)
2190}
2191
2192/// Builds intermediate expression tree from AST nodes.
2193pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2194    language: &L,
2195    diagnostics: &mut TemplateDiagnostics,
2196    build_ctx: &BuildContext<L::Property>,
2197    node: &ExpressionNode,
2198) -> TemplateParseResult<Expression<L::Property>> {
2199    template_parser::catch_aliases(diagnostics, node, |diagnostics, node| match &node.kind {
2200        ExpressionKind::Identifier(name) => {
2201            if let Some(make) = build_ctx.local_variables.get(name) {
2202                // Don't label a local variable with its name
2203                Ok(Expression::unlabeled(make()))
2204            } else if *name == "self" {
2205                // "self" is a special variable, so don't label it
2206                let make = build_ctx.self_variable;
2207                Ok(Expression::unlabeled(make()))
2208            } else {
2209                let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
2210                    .map_err(|err| {
2211                        err.extend_keyword_candidates(itertools::chain(
2212                            build_ctx.local_variables.keys().copied(),
2213                            ["self"],
2214                        ))
2215                    })?;
2216                Ok(Expression::with_label(property, *name))
2217            }
2218        }
2219        ExpressionKind::Boolean(value) => {
2220            let property = Literal(*value).into_dyn_wrapped();
2221            Ok(Expression::unlabeled(property))
2222        }
2223        ExpressionKind::Integer(value) => {
2224            let property = Literal(*value).into_dyn_wrapped();
2225            Ok(Expression::unlabeled(property))
2226        }
2227        ExpressionKind::String(value) => {
2228            let property = Literal(value.clone()).into_dyn_wrapped();
2229            Ok(Expression::unlabeled(property))
2230        }
2231        ExpressionKind::Pattern(_) => Err(TemplateParseError::expression(
2232            "String patterns may not be used as expression values",
2233            node.span,
2234        )),
2235        ExpressionKind::Unary(op, arg_node) => {
2236            let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
2237            Ok(Expression::unlabeled(property))
2238        }
2239        ExpressionKind::Binary(op, lhs_node, rhs_node) => {
2240            let property = build_binary_operation(
2241                language,
2242                diagnostics,
2243                build_ctx,
2244                *op,
2245                lhs_node,
2246                rhs_node,
2247                node.span,
2248            )?;
2249            Ok(Expression::unlabeled(property))
2250        }
2251        ExpressionKind::Concat(nodes) => {
2252            let templates = nodes
2253                .iter()
2254                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2255                .try_collect()?;
2256            let property = L::Property::wrap_template(Box::new(ConcatTemplate(templates)));
2257            Ok(Expression::unlabeled(property))
2258        }
2259        ExpressionKind::FunctionCall(function) => {
2260            let property = language.build_function(diagnostics, build_ctx, function)?;
2261            Ok(Expression::unlabeled(property))
2262        }
2263        ExpressionKind::MethodCall(method) => {
2264            let mut expression =
2265                build_expression(language, diagnostics, build_ctx, &method.object)?;
2266            expression.property = language.build_method(
2267                diagnostics,
2268                build_ctx,
2269                expression.property,
2270                &method.function,
2271            )?;
2272            expression.labels.push(method.function.name.to_owned());
2273            Ok(expression)
2274        }
2275        ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
2276            "Lambda cannot be defined here",
2277            node.span,
2278        )),
2279        ExpressionKind::AliasExpanded(..) => unreachable!(),
2280    })
2281}
2282
2283/// Builds template evaluation tree from AST nodes, with fresh build context.
2284pub fn build<'a, C, L>(
2285    language: &L,
2286    diagnostics: &mut TemplateDiagnostics,
2287    node: &ExpressionNode,
2288) -> TemplateParseResult<TemplateRenderer<'a, C>>
2289where
2290    C: Clone + 'a,
2291    L: TemplateLanguage<'a> + ?Sized,
2292    L::Property: WrapTemplateProperty<'a, C>,
2293{
2294    let self_placeholder = PropertyPlaceholder::new();
2295    let build_ctx = BuildContext {
2296        local_variables: HashMap::new(),
2297        self_variable: &|| self_placeholder.clone().into_dyn_wrapped(),
2298    };
2299    let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
2300    Ok(TemplateRenderer::new(template, self_placeholder))
2301}
2302
2303/// Parses text, expands aliases, then builds template evaluation tree.
2304pub fn parse<'a, C, L>(
2305    language: &L,
2306    diagnostics: &mut TemplateDiagnostics,
2307    template_text: &str,
2308    aliases_map: &TemplateAliasesMap,
2309) -> TemplateParseResult<TemplateRenderer<'a, C>>
2310where
2311    C: Clone + 'a,
2312    L: TemplateLanguage<'a> + ?Sized,
2313    L::Property: WrapTemplateProperty<'a, C>,
2314{
2315    let node = template_parser::parse(template_text, aliases_map)?;
2316    build(language, diagnostics, &node).map_err(|err| err.extend_alias_candidates(aliases_map))
2317}
2318
2319pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2320    language: &L,
2321    diagnostics: &mut TemplateDiagnostics,
2322    build_ctx: &BuildContext<L::Property>,
2323    node: &ExpressionNode,
2324) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>> {
2325    expect_expression_of_type(
2326        language,
2327        diagnostics,
2328        build_ctx,
2329        node,
2330        "Boolean",
2331        |expression| expression.try_into_boolean(),
2332    )
2333}
2334
2335pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2336    language: &L,
2337    diagnostics: &mut TemplateDiagnostics,
2338    build_ctx: &BuildContext<L::Property>,
2339    node: &ExpressionNode,
2340) -> TemplateParseResult<BoxedTemplateProperty<'a, i64>> {
2341    expect_expression_of_type(
2342        language,
2343        diagnostics,
2344        build_ctx,
2345        node,
2346        "Integer",
2347        |expression| expression.try_into_integer(),
2348    )
2349}
2350
2351/// If the given expression `node` is of `Integer` type, converts it to `isize`.
2352pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2353    language: &L,
2354    diagnostics: &mut TemplateDiagnostics,
2355    build_ctx: &BuildContext<L::Property>,
2356    node: &ExpressionNode,
2357) -> TemplateParseResult<BoxedTemplateProperty<'a, isize>> {
2358    let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2359    let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
2360    Ok(isize_property.into_dyn())
2361}
2362
2363/// If the given expression `node` is of `Integer` type, converts it to `usize`.
2364pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2365    language: &L,
2366    diagnostics: &mut TemplateDiagnostics,
2367    build_ctx: &BuildContext<L::Property>,
2368    node: &ExpressionNode,
2369) -> TemplateParseResult<BoxedTemplateProperty<'a, usize>> {
2370    let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2371    let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
2372    Ok(usize_property.into_dyn())
2373}
2374
2375pub fn expect_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2376    language: &L,
2377    diagnostics: &mut TemplateDiagnostics,
2378    build_ctx: &BuildContext<L::Property>,
2379    node: &ExpressionNode,
2380) -> TemplateParseResult<BoxedTemplateProperty<'a, String>> {
2381    // Since any formattable type can be converted to a string property, the
2382    // expected type is not a String.
2383    expect_expression_of_type(
2384        language,
2385        diagnostics,
2386        build_ctx,
2387        node,
2388        "Stringify",
2389        |expression| expression.try_into_stringify(),
2390    )
2391}
2392
2393pub fn expect_timestamp_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, Timestamp>> {
2399    expect_expression_of_type(
2400        language,
2401        diagnostics,
2402        build_ctx,
2403        node,
2404        "Timestamp",
2405        |expression| expression.try_into_timestamp(),
2406    )
2407}
2408
2409pub fn expect_serialize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2410    language: &L,
2411    diagnostics: &mut TemplateDiagnostics,
2412    build_ctx: &BuildContext<L::Property>,
2413    node: &ExpressionNode,
2414) -> TemplateParseResult<BoxedSerializeProperty<'a>> {
2415    expect_expression_of_type(
2416        language,
2417        diagnostics,
2418        build_ctx,
2419        node,
2420        "Serialize",
2421        |expression| expression.try_into_serialize(),
2422    )
2423}
2424
2425pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2426    language: &L,
2427    diagnostics: &mut TemplateDiagnostics,
2428    build_ctx: &BuildContext<L::Property>,
2429    node: &ExpressionNode,
2430) -> TemplateParseResult<Box<dyn Template + 'a>> {
2431    expect_expression_of_type(
2432        language,
2433        diagnostics,
2434        build_ctx,
2435        node,
2436        "Template",
2437        |expression| expression.try_into_template(),
2438    )
2439}
2440
2441pub fn expect_any_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2442    language: &L,
2443    diagnostics: &mut TemplateDiagnostics,
2444    build_ctx: &BuildContext<L::Property>,
2445    node: &ExpressionNode,
2446) -> TemplateParseResult<BoxedAnyProperty<'a>> {
2447    template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
2448        Ok(
2449            Box::new(build_expression(language, diagnostics, build_ctx, node)?)
2450                as BoxedAnyProperty<'a>,
2451        )
2452    })
2453}
2454
2455fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
2456    language: &L,
2457    diagnostics: &mut TemplateDiagnostics,
2458    build_ctx: &BuildContext<L::Property>,
2459    node: &ExpressionNode,
2460    expected_type: &str,
2461    f: impl FnOnce(Expression<L::Property>) -> Option<T>,
2462) -> TemplateParseResult<T> {
2463    template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
2464        let expression = build_expression(language, diagnostics, build_ctx, node)?;
2465        let actual_type = expression.type_name();
2466        f(expression)
2467            .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
2468    })
2469}
2470
2471#[cfg(test)]
2472mod tests {
2473    use assert_matches::assert_matches;
2474    use jj_lib::backend::MillisSinceEpoch;
2475    use jj_lib::config::StackedConfig;
2476
2477    use super::*;
2478    use crate::formatter;
2479    use crate::formatter::ColorFormatter;
2480    use crate::generic_templater;
2481    use crate::generic_templater::GenericTemplateLanguage;
2482
2483    #[derive(Clone, Debug, serde::Serialize)]
2484    struct Context;
2485
2486    type TestTemplateLanguage = GenericTemplateLanguage<'static, Context>;
2487    type TestTemplatePropertyKind = <TestTemplateLanguage as TemplateLanguage<'static>>::Property;
2488
2489    generic_templater::impl_self_property_wrapper!(Context);
2490
2491    /// Helper to set up template evaluation environment.
2492    struct TestTemplateEnv {
2493        language: TestTemplateLanguage,
2494        aliases_map: TemplateAliasesMap,
2495        color_rules: Vec<(Vec<String>, formatter::Style)>,
2496    }
2497
2498    impl TestTemplateEnv {
2499        fn new() -> Self {
2500            Self::with_config(StackedConfig::with_defaults())
2501        }
2502
2503        fn with_config(config: StackedConfig) -> Self {
2504            let settings = UserSettings::from_config(config).unwrap();
2505            Self {
2506                language: TestTemplateLanguage::new(&settings),
2507                aliases_map: TemplateAliasesMap::new(),
2508                color_rules: Vec::new(),
2509            }
2510        }
2511    }
2512
2513    impl TestTemplateEnv {
2514        fn add_keyword<F>(&mut self, name: &'static str, build: F)
2515        where
2516            F: Fn() -> TestTemplatePropertyKind + 'static,
2517        {
2518            self.language.add_keyword(name, move |_| Ok(build()));
2519        }
2520
2521        fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
2522            self.aliases_map.insert(decl, defn).unwrap();
2523        }
2524
2525        fn add_color(&mut self, label: &str, fg: crossterm::style::Color) {
2526            let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
2527            let style = formatter::Style {
2528                fg: Some(fg),
2529                ..Default::default()
2530            };
2531            self.color_rules.push((labels, style));
2532        }
2533
2534        fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, Context>> {
2535            parse(
2536                &self.language,
2537                &mut TemplateDiagnostics::new(),
2538                template,
2539                &self.aliases_map,
2540            )
2541        }
2542
2543        fn parse_err(&self, template: &str) -> String {
2544            let err = self
2545                .parse(template)
2546                .err()
2547                .expect("Got unexpected successful template rendering");
2548
2549            iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()).join("\n")
2550        }
2551
2552        fn parse_err_kind(&self, template: &str) -> TemplateParseErrorKind {
2553            self.parse(template)
2554                .err()
2555                .expect("Got unexpected successful template rendering")
2556                .kind()
2557                .clone()
2558        }
2559
2560        fn render_ok(&self, template: &str) -> String {
2561            let template = self.parse(template).unwrap();
2562            let mut output = Vec::new();
2563            let mut formatter =
2564                ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
2565            template.format(&Context, &mut formatter).unwrap();
2566            drop(formatter);
2567            String::from_utf8(output).unwrap()
2568        }
2569
2570        fn render_plain(&self, template: &str) -> String {
2571            let template = self.parse(template).unwrap();
2572            String::from_utf8(template.format_plain_text(&Context)).unwrap()
2573        }
2574    }
2575
2576    fn literal<'a, O>(value: O) -> TestTemplatePropertyKind
2577    where
2578        O: Clone + 'a,
2579        TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
2580    {
2581        Literal(value).into_dyn_wrapped()
2582    }
2583
2584    fn new_error_property<'a, O>(message: &'a str) -> TestTemplatePropertyKind
2585    where
2586        TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
2587    {
2588        Literal(())
2589            .and_then(|()| Err(TemplatePropertyError(message.into())))
2590            .into_dyn_wrapped()
2591    }
2592
2593    fn new_signature(name: &str, email: &str) -> Signature {
2594        Signature {
2595            name: name.to_owned(),
2596            email: email.to_owned(),
2597            timestamp: new_timestamp(0, 0),
2598        }
2599    }
2600
2601    fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
2602        Timestamp {
2603            timestamp: MillisSinceEpoch(msec),
2604            tz_offset,
2605        }
2606    }
2607
2608    #[test]
2609    fn test_parsed_tree() {
2610        let mut env = TestTemplateEnv::new();
2611        env.add_keyword("divergent", || literal(false));
2612        env.add_keyword("empty", || literal(true));
2613        env.add_keyword("hello", || literal("Hello".to_owned()));
2614
2615        // Empty
2616        insta::assert_snapshot!(env.render_ok(r#"  "#), @"");
2617
2618        // Single term with whitespace
2619        insta::assert_snapshot!(env.render_ok(r#"  hello.upper()  "#), @"HELLO");
2620
2621        // Multiple terms
2622        insta::assert_snapshot!(env.render_ok(r#"  hello.upper()  ++ true "#), @"HELLOtrue");
2623
2624        // Parenthesized single term
2625        insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
2626
2627        // Parenthesized multiple terms and concatenation
2628        insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
2629
2630        // Parenthesized "if" condition
2631        insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
2632
2633        // Parenthesized method chaining
2634        insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
2635
2636        // Multi-line method chaining
2637        insta::assert_snapshot!(env.render_ok("hello\n  .upper()"), @"HELLO");
2638    }
2639
2640    #[test]
2641    fn test_parse_error() {
2642        let mut env = TestTemplateEnv::new();
2643        env.add_keyword("description", || literal("".to_owned()));
2644        env.add_keyword("empty", || literal(true));
2645
2646        insta::assert_snapshot!(env.parse_err(r#"foo bar"#), @"
2647         --> 1:5
2648          |
2649        1 | foo bar
2650          |     ^---
2651          |
2652          = expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, `<`, `+`, `-`, `*`, `/`, or `%`
2653        ");
2654        insta::assert_snapshot!(env.parse_err("1 +"), @"
2655         --> 1:4
2656          |
2657        1 | 1 +
2658          |    ^---
2659          |
2660          = expected `!`, `-`, or <primary>
2661        ");
2662        insta::assert_snapshot!(env.parse_err("self.timestamp"), @"
2663         --> 1:6
2664          |
2665        1 | self.timestamp
2666          |      ^---
2667          |
2668          = expected <function>
2669        ");
2670
2671        insta::assert_snapshot!(env.parse_err(r#"foo"#), @"
2672         --> 1:1
2673          |
2674        1 | foo
2675          | ^-^
2676          |
2677          = Keyword `foo` doesn't exist
2678        ");
2679
2680        insta::assert_snapshot!(env.parse_err(r#"foo()"#), @"
2681         --> 1:1
2682          |
2683        1 | foo()
2684          | ^-^
2685          |
2686          = Function `foo` doesn't exist
2687        ");
2688        insta::assert_snapshot!(env.parse_err(r#"false()"#), @"
2689         --> 1:1
2690          |
2691        1 | false()
2692          | ^---^
2693          |
2694          = Expected identifier
2695        ");
2696
2697        insta::assert_snapshot!(env.parse_err(r#"!foo"#), @"
2698         --> 1:2
2699          |
2700        1 | !foo
2701          |  ^-^
2702          |
2703          = Keyword `foo` doesn't exist
2704        ");
2705        insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @"
2706         --> 1:9
2707          |
2708        1 | true && 123
2709          |         ^-^
2710          |
2711          = Expected expression of type `Boolean`, but actual type is `Integer`
2712        ");
2713        insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @"
2714         --> 1:1
2715          |
2716        1 | true == 1
2717          | ^-------^
2718          |
2719          = Cannot compare expressions of type `Boolean` and `Integer`
2720        ");
2721        insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @"
2722         --> 1:1
2723          |
2724        1 | true != 'a'
2725          | ^---------^
2726          |
2727          = Cannot compare expressions of type `Boolean` and `String`
2728        ");
2729        insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @"
2730         --> 1:1
2731          |
2732        1 | 1 == true
2733          | ^-------^
2734          |
2735          = Cannot compare expressions of type `Integer` and `Boolean`
2736        ");
2737        insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @"
2738         --> 1:1
2739          |
2740        1 | 1 != 'a'
2741          | ^------^
2742          |
2743          = Cannot compare expressions of type `Integer` and `String`
2744        ");
2745        insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @"
2746         --> 1:1
2747          |
2748        1 | 'a' == true
2749          | ^---------^
2750          |
2751          = Cannot compare expressions of type `String` and `Boolean`
2752        ");
2753        insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @"
2754         --> 1:1
2755          |
2756        1 | 'a' != 1
2757          | ^------^
2758          |
2759          = Cannot compare expressions of type `String` and `Integer`
2760        ");
2761        insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
2762         --> 1:1
2763          |
2764        1 | 'a' == label("", "")
2765          | ^------------------^
2766          |
2767          = Cannot compare expressions of type `String` and `Template`
2768        "#);
2769        insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @"
2770         --> 1:1
2771          |
2772        1 | 'a' > 1
2773          | ^-----^
2774          |
2775          = Cannot compare expressions of type `String` and `Integer`
2776        ");
2777
2778        insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @"
2779         --> 1:26
2780          |
2781        1 | description.first_line().foo()
2782          |                          ^-^
2783          |
2784          = Method `foo` doesn't exist for type `String`
2785        ");
2786
2787        insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @"
2788         --> 1:1
2789          |
2790        1 | 10000000000000000000
2791          | ^------------------^
2792          |
2793          = Invalid integer literal
2794        number too large to fit in target type
2795        ");
2796        insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @"
2797         --> 1:4
2798          |
2799        1 | 42.foo()
2800          |    ^-^
2801          |
2802          = Method `foo` doesn't exist for type `Integer`
2803        ");
2804        insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @"
2805         --> 1:3
2806          |
2807        1 | (-empty)
2808          |   ^---^
2809          |
2810          = Expected expression of type `Integer`, but actual type is `Boolean`
2811        ");
2812
2813        insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r#"
2814         --> 1:18
2815          |
2816        1 | ("foo" ++ "bar").baz()
2817          |                  ^-^
2818          |
2819          = Method `baz` doesn't exist for type `Template`
2820        "#);
2821
2822        insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @"
2823         --> 1:22
2824          |
2825        1 | description.contains()
2826          |                      ^
2827          |
2828          = Function `contains`: Expected 1 arguments
2829        ");
2830
2831        insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r#"
2832         --> 1:24
2833          |
2834        1 | description.first_line("foo")
2835          |                        ^---^
2836          |
2837          = Function `first_line`: Expected 0 arguments
2838        "#);
2839
2840        insta::assert_snapshot!(env.parse_err(r#"label()"#), @"
2841         --> 1:7
2842          |
2843        1 | label()
2844          |       ^
2845          |
2846          = Function `label`: Expected 2 arguments
2847        ");
2848        insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r#"
2849         --> 1:7
2850          |
2851        1 | label("foo", "bar", "baz")
2852          |       ^-----------------^
2853          |
2854          = Function `label`: Expected 2 arguments
2855        "#);
2856
2857        insta::assert_snapshot!(env.parse_err(r#"if()"#), @"
2858         --> 1:4
2859          |
2860        1 | if()
2861          |    ^
2862          |
2863          = Function `if`: Expected 2 to 3 arguments
2864        ");
2865        insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r#"
2866         --> 1:4
2867          |
2868        1 | if("foo", "bar", "baz", "quux")
2869          |    ^-------------------------^
2870          |
2871          = Function `if`: Expected 2 to 3 arguments
2872        "#);
2873
2874        insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
2875         --> 1:37
2876          |
2877        1 | pad_start("foo", fill_char = "bar", "baz")
2878          |                                     ^---^
2879          |
2880          = Function `pad_start`: Positional argument follows keyword argument
2881        "#);
2882
2883        insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r#"
2884         --> 1:4
2885          |
2886        1 | if(label("foo", "bar"), "baz")
2887          |    ^-----------------^
2888          |
2889          = Expected expression of type `Boolean`, but actual type is `Template`
2890        "#);
2891
2892        insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @"
2893         --> 1:1
2894          |
2895        1 | |x| description
2896          | ^-------------^
2897          |
2898          = Lambda cannot be defined here
2899        ");
2900    }
2901
2902    #[test]
2903    fn test_self_keyword() {
2904        let mut env = TestTemplateEnv::new();
2905        env.add_keyword("say_hello", || literal("Hello".to_owned()));
2906
2907        insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
2908        insta::assert_snapshot!(env.parse_err(r#"self"#), @"
2909         --> 1:1
2910          |
2911        1 | self
2912          | ^--^
2913          |
2914          = Expected expression of type `Template`, but actual type is `Self`
2915        ");
2916    }
2917
2918    #[test]
2919    fn test_boolean_cast() {
2920        let mut env = TestTemplateEnv::new();
2921
2922        insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
2923        insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
2924
2925        env.add_keyword("sl0", || literal::<Vec<String>>(vec![]));
2926        env.add_keyword("sl1", || literal(vec!["".to_owned()]));
2927        insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
2928        insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
2929
2930        // No implicit cast of integer
2931        insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @"
2932         --> 1:4
2933          |
2934        1 | if(0, true, false)
2935          |    ^
2936          |
2937          = Expected expression of type `Boolean`, but actual type is `Integer`
2938        ");
2939
2940        // Optional integer can be converted to boolean, and Some(0) is truthy.
2941        env.add_keyword("none_i64", || literal(None::<i64>));
2942        env.add_keyword("some_i64", || literal(Some(0)));
2943        insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
2944        insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
2945
2946        // Property errors do not evaluate
2947        insta::assert_snapshot!(
2948            env.render_ok("if(-none_i64 == 1, true, false)"),
2949            @"<Error: No Integer available>"
2950        );
2951
2952        insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r#"
2953         --> 1:4
2954          |
2955        1 | if(label("", ""), true, false)
2956          |    ^-----------^
2957          |
2958          = Expected expression of type `Boolean`, but actual type is `Template`
2959        "#);
2960        insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @"
2961         --> 1:4
2962          |
2963        1 | if(sl0.map(|x| x), true, false)
2964          |    ^------------^
2965          |
2966          = Expected expression of type `Boolean`, but actual type is `AnyList`
2967        ");
2968
2969        env.add_keyword("empty_email", || literal(Email("".to_owned())));
2970        env.add_keyword("nonempty_email", || {
2971            literal(Email("local@domain".to_owned()))
2972        });
2973        insta::assert_snapshot!(env.render_ok(r#"if(empty_email, true, false)"#), @"false");
2974        insta::assert_snapshot!(env.render_ok(r#"if(nonempty_email, true, false)"#), @"true");
2975
2976        // even boolean config values must be extracted
2977        env.add_keyword("config_bool", || literal(ConfigValue::from(true)));
2978        insta::assert_snapshot!(env.parse_err("if(config_bool, true, false)"), @"
2979         --> 1:4
2980          |
2981        1 | if(config_bool, true, false)
2982          |    ^---------^
2983          |
2984          = Expected expression of type `Boolean`, but actual type is `ConfigValue`
2985        ");
2986
2987        // misc uncastable types
2988        env.add_keyword("signature", || {
2989            literal(new_signature("Test User", "test.user@example.com"))
2990        });
2991        env.add_keyword("size_hint", || literal((5, None)));
2992        env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
2993        env.add_keyword("timestamp_range", || {
2994            literal(TimestampRange {
2995                start: new_timestamp(0, 0),
2996                end: new_timestamp(0, 0),
2997            })
2998        });
2999        assert_matches!(
3000            env.parse_err_kind("if(signature, true, false)"),
3001            TemplateParseErrorKind::Expression(_)
3002        );
3003        assert_matches!(
3004            env.parse_err_kind("if(size_hint, true, false)"),
3005            TemplateParseErrorKind::Expression(_)
3006        );
3007        assert_matches!(
3008            env.parse_err_kind("if(timestamp, true, false)"),
3009            TemplateParseErrorKind::Expression(_)
3010        );
3011        assert_matches!(
3012            env.parse_err_kind("if(timestamp_range, true, false)"),
3013            TemplateParseErrorKind::Expression(_)
3014        );
3015    }
3016
3017    #[test]
3018    fn test_arithmetic_operation() {
3019        let mut env = TestTemplateEnv::new();
3020        env.add_keyword("none_i64", || literal(None::<i64>));
3021        env.add_keyword("some_i64", || literal(Some(1)));
3022        env.add_keyword("i64_min", || literal(i64::MIN));
3023        env.add_keyword("i64_max", || literal(i64::MAX));
3024
3025        insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
3026        insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
3027        insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
3028        insta::assert_snapshot!(env.render_ok(r#"1 + 2"#), @"3");
3029        insta::assert_snapshot!(env.render_ok(r#"2 * 3"#), @"6");
3030        insta::assert_snapshot!(env.render_ok(r#"1 + 2 * 3"#), @"7");
3031        insta::assert_snapshot!(env.render_ok(r#"4 / 2"#), @"2");
3032        insta::assert_snapshot!(env.render_ok(r#"5 / 2"#), @"2");
3033        insta::assert_snapshot!(env.render_ok(r#"5 % 2"#), @"1");
3034
3035        // Since methods of the contained value can be invoked, it makes sense
3036        // to apply operators to optional integers as well.
3037        insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
3038        insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
3039        insta::assert_snapshot!(env.render_ok(r#"some_i64 + some_i64"#), @"2");
3040        insta::assert_snapshot!(env.render_ok(r#"some_i64 + none_i64"#), @"<Error: No Integer available>");
3041        insta::assert_snapshot!(env.render_ok(r#"none_i64 + some_i64"#), @"<Error: No Integer available>");
3042        insta::assert_snapshot!(env.render_ok(r#"none_i64 + none_i64"#), @"<Error: No Integer available>");
3043
3044        // No panic on integer overflow.
3045        insta::assert_snapshot!(
3046            env.render_ok(r#"-i64_min"#),
3047            @"<Error: Attempt to negate with overflow>");
3048        insta::assert_snapshot!(
3049            env.render_ok(r#"i64_max + 1"#),
3050            @"<Error: Attempt to add with overflow>");
3051        insta::assert_snapshot!(
3052            env.render_ok(r#"i64_min - 1"#),
3053            @"<Error: Attempt to subtract with overflow>");
3054        insta::assert_snapshot!(
3055            env.render_ok(r#"i64_max * 2"#),
3056            @"<Error: Attempt to multiply with overflow>");
3057        insta::assert_snapshot!(
3058            env.render_ok(r#"i64_min / -1"#),
3059            @"<Error: Attempt to divide with overflow>");
3060        insta::assert_snapshot!(
3061            env.render_ok(r#"1 / 0"#),
3062            @"<Error: Attempt to divide by zero>");
3063        insta::assert_snapshot!(
3064            env.render_ok("i64_min % -1"),
3065            @"<Error: Attempt to divide with overflow>");
3066        insta::assert_snapshot!(
3067            env.render_ok(r#"1 % 0"#),
3068            @"<Error: Attempt to divide by zero>");
3069    }
3070
3071    #[test]
3072    fn test_relational_operation() {
3073        let mut env = TestTemplateEnv::new();
3074        env.add_keyword("none_i64", || literal(None::<i64>));
3075        env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3076        env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3077
3078        insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true");
3079        insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false");
3080        insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true");
3081        insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false");
3082        insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true");
3083        insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false");
3084        insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true");
3085        insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false");
3086
3087        // none < some
3088        insta::assert_snapshot!(env.render_ok(r#"none_i64 < some_i64_0"#), @"true");
3089        insta::assert_snapshot!(env.render_ok(r#"some_i64_0 > some_i64_1"#), @"false");
3090        insta::assert_snapshot!(env.render_ok(r#"none_i64 < 0"#), @"true");
3091        insta::assert_snapshot!(env.render_ok(r#"1 > some_i64_0"#), @"true");
3092
3093        // invalid comparisons
3094        assert_matches!(
3095            env.parse_err_kind("42 >= true"),
3096            TemplateParseErrorKind::Expression(_)
3097        );
3098        assert_matches!(
3099            env.parse_err_kind("none_i64 >= true"),
3100            TemplateParseErrorKind::Expression(_)
3101        );
3102
3103        // un-comparable types
3104        env.add_keyword("str_list", || {
3105            literal(vec!["foo".to_owned(), "bar".to_owned()])
3106        });
3107        env.add_keyword("cfg_val", || {
3108            literal(ConfigValue::from_iter([("foo", "bar")]))
3109        });
3110        env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(1))));
3111        env.add_keyword("signature", || {
3112            literal(new_signature("User", "user@example.com"))
3113        });
3114        env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
3115        env.add_keyword("size_hint", || literal((10, None)));
3116        env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3117        env.add_keyword("timestamp_range", || {
3118            literal(TimestampRange {
3119                start: new_timestamp(0, 0),
3120                end: new_timestamp(0, 0),
3121            })
3122        });
3123        assert_matches!(
3124            env.parse_err_kind("'a' >= 'a'"),
3125            TemplateParseErrorKind::Expression(_)
3126        );
3127        assert_matches!(
3128            env.parse_err_kind("str_list >= str_list"),
3129            TemplateParseErrorKind::Expression(_)
3130        );
3131        assert_matches!(
3132            env.parse_err_kind("true >= true"),
3133            TemplateParseErrorKind::Expression(_)
3134        );
3135        assert_matches!(
3136            env.parse_err_kind("cfg_val >= cfg_val"),
3137            TemplateParseErrorKind::Expression(_)
3138        );
3139        assert_matches!(
3140            env.parse_err_kind("some_cfg >= some_cfg"),
3141            TemplateParseErrorKind::Expression(_)
3142        );
3143        assert_matches!(
3144            env.parse_err_kind("signature >= signature"),
3145            TemplateParseErrorKind::Expression(_)
3146        );
3147        assert_matches!(
3148            env.parse_err_kind("email >= email"),
3149            TemplateParseErrorKind::Expression(_)
3150        );
3151        assert_matches!(
3152            env.parse_err_kind("size_hint >= size_hint"),
3153            TemplateParseErrorKind::Expression(_)
3154        );
3155        assert_matches!(
3156            env.parse_err_kind("timestamp >= timestamp"),
3157            TemplateParseErrorKind::Expression(_)
3158        );
3159        assert_matches!(
3160            env.parse_err_kind("timestamp_range >= timestamp_range"),
3161            TemplateParseErrorKind::Expression(_)
3162        );
3163        assert_matches!(
3164            env.parse_err_kind("label('', '') >= label('', '')"),
3165            TemplateParseErrorKind::Expression(_)
3166        );
3167        assert_matches!(
3168            env.parse_err_kind("str_list.map(|s| s) >= str_list.map(|s| s)"),
3169            TemplateParseErrorKind::Expression(_)
3170        );
3171    }
3172
3173    #[test]
3174    fn test_logical_operation() {
3175        let mut env = TestTemplateEnv::new();
3176        env.add_keyword("none_i64", || literal::<Option<i64>>(None));
3177        env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3178        env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3179        env.add_keyword("email1", || literal(Email("local-1@domain".to_owned())));
3180        env.add_keyword("email2", || literal(Email("local-2@domain".to_owned())));
3181
3182        insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
3183        insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
3184        insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
3185        insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
3186        insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
3187        insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
3188        insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
3189
3190        insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
3191        insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
3192        insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
3193        insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
3194        insta::assert_snapshot!(env.render_ok(r#"none_i64 == none_i64"#), @"true");
3195        insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != some_i64_0"#), @"false");
3196        insta::assert_snapshot!(env.render_ok(r#"none_i64 == 0"#), @"false");
3197        insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != 0"#), @"false");
3198        insta::assert_snapshot!(env.render_ok(r#"1 == some_i64_1"#), @"true");
3199
3200        insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
3201        insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
3202        insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
3203        insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
3204        insta::assert_snapshot!(env.render_ok(r#"email1 == email1"#), @"true");
3205        insta::assert_snapshot!(env.render_ok(r#"email1 == email2"#), @"false");
3206        insta::assert_snapshot!(env.render_ok(r#"email1 == 'local-1@domain'"#), @"true");
3207        insta::assert_snapshot!(env.render_ok(r#"email1 != 'local-2@domain'"#), @"true");
3208        insta::assert_snapshot!(env.render_ok(r#"'local-1@domain' == email1"#), @"true");
3209        insta::assert_snapshot!(env.render_ok(r#"'local-2@domain' != email1"#), @"true");
3210
3211        insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
3212        insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
3213
3214        // Short-circuiting
3215        env.add_keyword("bad_bool", || new_error_property::<bool>("Bad"));
3216        insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
3217        insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
3218        insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
3219        insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
3220
3221        // Invalid comparisons
3222        assert_matches!(
3223            env.parse_err_kind("some_i64_0 == '0'"),
3224            TemplateParseErrorKind::Expression(_)
3225        );
3226        assert_matches!(
3227            env.parse_err_kind("email1 == 42"),
3228            TemplateParseErrorKind::Expression(_)
3229        );
3230
3231        // Un-comparable types
3232        env.add_keyword("str_list", || {
3233            literal(vec!["foo".to_owned(), "bar".to_owned()])
3234        });
3235        env.add_keyword("cfg_val", || {
3236            literal(ConfigValue::from_iter([("foo", "bar")]))
3237        });
3238        env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(true))));
3239        env.add_keyword("signature", || {
3240            literal(new_signature("User", "user@example.com"))
3241        });
3242        env.add_keyword("size_hint", || literal((10, None)));
3243        env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3244        env.add_keyword("timestamp_range", || {
3245            literal(TimestampRange {
3246                start: new_timestamp(0, 0),
3247                end: new_timestamp(0, 0),
3248            })
3249        });
3250        assert_matches!(
3251            env.parse_err_kind("str_list == str_list"),
3252            TemplateParseErrorKind::Expression(_)
3253        );
3254        assert_matches!(
3255            env.parse_err_kind("cfg_val == cfg_val"),
3256            TemplateParseErrorKind::Expression(_)
3257        );
3258        assert_matches!(
3259            env.parse_err_kind("some_cfg == some_cfg"),
3260            TemplateParseErrorKind::Expression(_)
3261        );
3262        assert_matches!(
3263            env.parse_err_kind("signature == signature"),
3264            TemplateParseErrorKind::Expression(_)
3265        );
3266        assert_matches!(
3267            env.parse_err_kind("size_hint == size_hint"),
3268            TemplateParseErrorKind::Expression(_)
3269        );
3270        assert_matches!(
3271            env.parse_err_kind("timestamp == timestamp"),
3272            TemplateParseErrorKind::Expression(_)
3273        );
3274        assert_matches!(
3275            env.parse_err_kind("timestamp_range == timestamp_range"),
3276            TemplateParseErrorKind::Expression(_)
3277        );
3278        assert_matches!(
3279            env.parse_err_kind("label('', '') == label('', '')"),
3280            TemplateParseErrorKind::Expression(_)
3281        );
3282        assert_matches!(
3283            env.parse_err_kind("str_list.map(|s| s) == str_list.map(|s| s)"),
3284            TemplateParseErrorKind::Expression(_)
3285        );
3286    }
3287
3288    #[test]
3289    fn test_list_method() {
3290        let mut env = TestTemplateEnv::new();
3291        env.add_keyword("empty", || literal(true));
3292        env.add_keyword("sep", || literal("sep".to_owned()));
3293
3294        insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
3295        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
3296
3297        insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
3298        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
3299        // Null separator
3300        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
3301        // Keyword as separator
3302        insta::assert_snapshot!(
3303            env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
3304            @"aSEPbSEPc");
3305
3306        insta::assert_snapshot!(
3307            env.render_ok(r#""a\nbb\nc".lines().filter(|s| s.len() == 1)"#),
3308            @"a c");
3309
3310        insta::assert_snapshot!(
3311            env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
3312            @"aa bb cc");
3313
3314        // Test any() method
3315        insta::assert_snapshot!(
3316            env.render_ok(r#""a\nb\nc".lines().any(|s| s == "b")"#),
3317            @"true");
3318        insta::assert_snapshot!(
3319            env.render_ok(r#""a\nb\nc".lines().any(|s| s == "d")"#),
3320            @"false");
3321        insta::assert_snapshot!(
3322            env.render_ok(r#""".lines().any(|s| s == "a")"#),
3323            @"false");
3324        // any() with more complex predicate
3325        insta::assert_snapshot!(
3326            env.render_ok(r#""ax\nbb\nc".lines().any(|s| s.contains("x"))"#),
3327            @"true");
3328        insta::assert_snapshot!(
3329            env.render_ok(r#""a\nbb\nc".lines().any(|s| s.len() > 1)"#),
3330            @"true");
3331
3332        // Test all() method
3333        insta::assert_snapshot!(
3334            env.render_ok(r#""a\nb\nc".lines().all(|s| s.len() == 1)"#),
3335            @"true");
3336        insta::assert_snapshot!(
3337            env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() == 1)"#),
3338            @"false");
3339        // Empty list returns true for all()
3340        insta::assert_snapshot!(
3341            env.render_ok(r#""".lines().all(|s| s == "a")"#),
3342            @"true");
3343        // all() with more complex predicate
3344        insta::assert_snapshot!(
3345            env.render_ok(r#""ax\nbx\ncx".lines().all(|s| s.ends_with("x"))"#),
3346            @"true");
3347        insta::assert_snapshot!(
3348            env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() < 3)"#),
3349            @"true");
3350
3351        // Combining any/all with filter
3352        insta::assert_snapshot!(
3353            env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).any(|s| s == "bb")"#),
3354            @"true");
3355        insta::assert_snapshot!(
3356            env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).all(|s| s.len() >= 2)"#),
3357            @"true");
3358
3359        // Nested any/all operations
3360        insta::assert_snapshot!(
3361            env.render_ok(r#"if("a\nb".lines().any(|s| s == "a"), "found", "not found")"#),
3362            @"found");
3363        insta::assert_snapshot!(
3364            env.render_ok(r#"if("a\nb".lines().all(|s| s.len() == 1), "all single", "not all")"#),
3365            @"all single");
3366
3367        // Global keyword in item template
3368        insta::assert_snapshot!(
3369            env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
3370            @"atrue btrue ctrue");
3371        // Global keyword in item template shadowing 'self'
3372        insta::assert_snapshot!(
3373            env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
3374            @"atrue btrue ctrue");
3375        // Override global keyword 'empty'
3376        insta::assert_snapshot!(
3377            env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
3378            @"a b c");
3379        // Nested map operations
3380        insta::assert_snapshot!(
3381            env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
3382            @"ax ay bx by cx cy");
3383        // Nested map/join operations
3384        insta::assert_snapshot!(
3385            env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
3386            @"ax,ay;bx,by;cx,cy");
3387        // Nested string operations
3388        insta::assert_snapshot!(
3389            env.render_ok(r#""!  a\n!b\nc\n   end".remove_suffix("end").trim_end().lines().map(|s| s.remove_prefix("!").trim_start())"#),
3390            @"a b c");
3391
3392        // Lambda expression in alias
3393        env.add_alias("identity", "|x| x");
3394        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
3395
3396        // Not a lambda expression
3397        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r#"
3398         --> 1:17
3399          |
3400        1 | "a".lines().map(empty)
3401          |                 ^---^
3402          |
3403          = Expected lambda expression
3404        "#);
3405        // Bad lambda parameter count
3406        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r#"
3407         --> 1:18
3408          |
3409        1 | "a".lines().map(|| "")
3410          |                  ^
3411          |
3412          = Expected 1 lambda parameters
3413        "#);
3414        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r#"
3415         --> 1:18
3416          |
3417        1 | "a".lines().map(|a, b| "")
3418          |                  ^--^
3419          |
3420          = Expected 1 lambda parameters
3421        "#);
3422        // Bad lambda output
3423        insta::assert_snapshot!(env.parse_err(r#""a".lines().filter(|s| s ++ "\n")"#), @r#"
3424         --> 1:24
3425          |
3426        1 | "a".lines().filter(|s| s ++ "\n")
3427          |                        ^-------^
3428          |
3429          = Expected expression of type `Boolean`, but actual type is `Template`
3430        "#);
3431
3432        // Error in any() and all()
3433        insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|s| s.len())"#), @r#"
3434         --> 1:21
3435          |
3436        1 | "a".lines().any(|s| s.len())
3437          |                     ^-----^
3438          |
3439          = Expected expression of type `Boolean`, but actual type is `Integer`
3440        "#);
3441        // Bad lambda output for all()
3442        insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|s| s ++ "x")"#), @r#"
3443         --> 1:21
3444          |
3445        1 | "a".lines().all(|s| s ++ "x")
3446          |                     ^------^
3447          |
3448          = Expected expression of type `Boolean`, but actual type is `Template`
3449        "#);
3450        // Wrong parameter count for any()
3451        insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|| true)"#), @r#"
3452         --> 1:18
3453          |
3454        1 | "a".lines().any(|| true)
3455          |                  ^
3456          |
3457          = Expected 1 lambda parameters
3458        "#);
3459        // Wrong parameter count for all()
3460        insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|a, b| true)"#), @r#"
3461         --> 1:18
3462          |
3463        1 | "a".lines().all(|a, b| true)
3464          |                  ^--^
3465          |
3466          = Expected 1 lambda parameters
3467        "#);
3468        // Error in lambda expression
3469        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r#"
3470         --> 1:23
3471          |
3472        1 | "a".lines().map(|s| s.unknown())
3473          |                       ^-----^
3474          |
3475          = Method `unknown` doesn't exist for type `String`
3476        "#);
3477        // Error in lambda alias
3478        env.add_alias("too_many_params", "|x, y| x");
3479        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
3480         --> 1:17
3481          |
3482        1 | "a".lines().map(too_many_params)
3483          |                 ^-------------^
3484          |
3485          = In alias `too_many_params`
3486         --> 1:2
3487          |
3488        1 | |x, y| x
3489          |  ^--^
3490          |
3491          = Expected 1 lambda parameters
3492        "#);
3493
3494        // List.first()
3495        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().first()"#), @"a");
3496        insta::assert_snapshot!(env.render_ok(r#""".lines().first()"#), @"<Error: List is empty>");
3497
3498        // List.last()
3499        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().last()"#), @"c");
3500        insta::assert_snapshot!(env.render_ok(r#""".lines().last()"#), @"<Error: List is empty>");
3501
3502        // List.get(index)
3503        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(0)"#), @"a");
3504        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(1)"#), @"b");
3505        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(2)"#), @"c");
3506        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(3)"#), @"<Error: Index 3 out of bounds>");
3507        insta::assert_snapshot!(env.render_ok(r#""".lines().get(0)"#), @"<Error: Index 0 out of bounds>");
3508
3509        // List.reverse()
3510        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().reverse().join("|")"#), @"c|b|a");
3511        insta::assert_snapshot!(env.render_ok(r#""".lines().reverse().join("|")"#), @"");
3512
3513        // List.skip(count)
3514        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(0).join("|")"#), @"a|b|c");
3515        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(1).join("|")"#), @"b|c");
3516        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(2).join("|")"#), @"c");
3517        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(3).join("|")"#), @"");
3518        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(10).join("|")"#), @"");
3519
3520        // List.take(count)
3521        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(0).join("|")"#), @"");
3522        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(1).join("|")"#), @"a");
3523        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(2).join("|")"#), @"a|b");
3524        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(3).join("|")"#), @"a|b|c");
3525        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(10).join("|")"#), @"a|b|c");
3526
3527        // Combining skip and take
3528        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\nd".lines().skip(1).take(2).join("|")"#), @"b|c");
3529    }
3530
3531    #[test]
3532    fn test_string_method() {
3533        let mut env = TestTemplateEnv::new();
3534        env.add_keyword("description", || literal("description 1".to_owned()));
3535        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
3536
3537        insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
3538        insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
3539        insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
3540
3541        insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
3542        insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
3543        insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
3544        insta::assert_snapshot!(
3545            env.render_ok(r#""description 123".contains(description.first_line())"#),
3546            @"true");
3547
3548        // String patterns are not stringifiable
3549        insta::assert_snapshot!(env.parse_err(r#""fa".starts_with(regex:'[a-f]o+')"#), @r#"
3550         --> 1:18
3551          |
3552        1 | "fa".starts_with(regex:'[a-f]o+')
3553          |                  ^-------------^
3554          |
3555          = String patterns may not be used as expression values
3556        "#);
3557
3558        // inner template error should propagate
3559        insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
3560        insta::assert_snapshot!(
3561            env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
3562        insta::assert_snapshot!(
3563            env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
3564
3565        insta::assert_snapshot!(env.render_ok(r#""fooo".match(regex:'[a-f]o+')"#), @"fooo");
3566        insta::assert_snapshot!(env.render_ok(r#""fa".match(regex:'[a-f]o+')"#), @"");
3567        insta::assert_snapshot!(env.render_ok(r#""hello".match(regex:"h(ell)o")"#), @"hello");
3568        insta::assert_snapshot!(env.render_ok(r#""HEllo".match(regex-i:"h(ell)o")"#), @"HEllo");
3569        insta::assert_snapshot!(env.render_ok(r#""hEllo".match(glob:"h*o")"#), @"hEllo");
3570        insta::assert_snapshot!(env.render_ok(r#""Hello".match(glob:"h*o")"#), @"");
3571        insta::assert_snapshot!(env.render_ok(r#""HEllo".match(glob-i:"h*o")"#), @"HEllo");
3572        insta::assert_snapshot!(env.render_ok(r#""hello".match("he")"#), @"he");
3573        insta::assert_snapshot!(env.render_ok(r#""hello".match(substring:"he")"#), @"he");
3574        insta::assert_snapshot!(env.render_ok(r#""hello".match(exact:"he")"#), @"");
3575
3576        // Evil regexes can cause invalid UTF-8 output, which nothing can
3577        // really be done about given we're matching against non-UTF-8 stuff a
3578        // lot as well.
3579        insta::assert_snapshot!(env.render_ok(r#""🥺".match(regex:'(?-u)^(?:.)')"#), @"<Error: incomplete utf-8 byte sequence from index 0>");
3580
3581        insta::assert_snapshot!(env.parse_err(r#""hello".match(false)"#), @r#"
3582         --> 1:15
3583          |
3584        1 | "hello".match(false)
3585          |               ^---^
3586          |
3587          = Expected string pattern
3588        "#);
3589        insta::assert_snapshot!(env.parse_err(r#""🥺".match(not-a-pattern:"abc")"#), @r#"
3590         --> 1:11
3591          |
3592        1 | "🥺".match(not-a-pattern:"abc")
3593          |           ^-----------------^
3594          |
3595          = Bad string pattern
3596        Invalid string pattern kind `not-a-pattern:`
3597        "#);
3598
3599        insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
3600        insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
3601
3602        insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
3603        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
3604
3605        insta::assert_snapshot!(env.render_ok(r#""".split(",")"#), @"");
3606        insta::assert_snapshot!(env.render_ok(r#""a,b,c".split(",")"#), @"a b c");
3607        insta::assert_snapshot!(env.render_ok(r#""a::b::c::d".split("::")"#), @"a b c d");
3608        insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 0)"#), @"");
3609        insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 2)"#), @"a b,c,d");
3610        insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 3)"#), @"a b c,d");
3611        insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 10)"#), @"a b c d");
3612        insta::assert_snapshot!(env.render_ok(r#""abc".split(",", -1)"#), @"<Error: out of range integral type conversion attempted>");
3613        insta::assert_snapshot!(env.render_ok(r#"json("a1b2c3".split(regex:'\d+'))"#), @r#"["a","b","c",""]"#);
3614        insta::assert_snapshot!(env.render_ok(r#""foo  bar   baz".split(regex:'\s+')"#), @"foo bar baz");
3615        insta::assert_snapshot!(env.render_ok(r#""a1b2c3d4".split(regex:'\d+', 3)"#), @"a b c3d4");
3616        insta::assert_snapshot!(env.render_ok(r#"json("hello world".split(regex-i:"WORLD"))"#), @r#"["hello ",""]"#);
3617
3618        insta::assert_snapshot!(env.render_ok("''.upper()"), @"");
3619        insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.upper()"), @"ABCABC 123!@#");
3620        insta::assert_snapshot!(env.render_ok("''.lower()"), @"");
3621        insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.lower()"), @"abcabc 123!@#");
3622
3623        insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
3624        insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
3625        insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
3626        insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
3627        insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
3628        insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
3629
3630        insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
3631        insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
3632        insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
3633        insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
3634        insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
3635        insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
3636
3637        insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
3638        insta::assert_snapshot!(
3639            env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
3640            @"testing");
3641
3642        insta::assert_snapshot!(
3643            env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
3644            @"bar@my.example.com");
3645        insta::assert_snapshot!(
3646            env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
3647            @"bar");
3648
3649        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim()"#), @"");
3650        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim()"#), @"foo  bar");
3651
3652        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim_start()"#), @"");
3653        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim_start()"#), @"foo  bar");
3654
3655        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim_end()"#), @"");
3656        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim_end()"#), @"\n\r foo  bar");
3657
3658        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
3659        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
3660        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
3661        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
3662        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
3663        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
3664        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
3665        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
3666
3667        // non-ascii characters
3668        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
3669        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
3670        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
3671        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
3672        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
3673        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
3674        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
3675        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
3676        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
3677        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
3678        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
3679
3680        // ranges with end > start are empty
3681        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
3682        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
3683
3684        insta::assert_snapshot!(env.render_ok(r#""hello".escape_json()"#), @r#""hello""#);
3685        insta::assert_snapshot!(env.render_ok(r#""he \n ll \n \" o".escape_json()"#), @r#""he \n ll \n \" o""#);
3686
3687        // simple substring replacement
3688        insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", "jj")"#), @"hello jj");
3689        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj")"#), @"hello jj jj");
3690        insta::assert_snapshot!(env.render_ok(r#""hello".replace("missing", "jj")"#), @"hello");
3691
3692        // replace with limit >=0
3693        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 0)"#), @"hello world world");
3694        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 1)"#), @"hello jj world");
3695        insta::assert_snapshot!(env.render_ok(r#""hello world world world".replace("world", "jj", 2)"#), @"hello jj jj world");
3696
3697        // replace with limit <0 (error due to negative limit)
3698        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -1)"#), @"<Error: out of range integral type conversion attempted>");
3699        insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -5)"#), @"<Error: out of range integral type conversion attempted>");
3700
3701        // replace with regex patterns
3702        insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X")"#), @"helloXworldX");
3703        insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X", 1)"#), @"helloXworld456");
3704
3705        // replace with regex patterns (capture groups)
3706        insta::assert_snapshot!(env.render_ok(r#""HELLO    WORLD".replace(regex-i:"(hello) +(world)", "$2 $1")"#), @"WORLD HELLO");
3707        insta::assert_snapshot!(env.render_ok(r#""abc123".replace(regex:"([a-z]+)([0-9]+)", "$2-$1")"#), @"123-abc");
3708        insta::assert_snapshot!(env.render_ok(r#""foo123bar".replace(regex:'\d+', "[$0]")"#), @"foo[123]bar");
3709
3710        // replace with regex patterns (case insensitive)
3711        insta::assert_snapshot!(env.render_ok(r#""Hello World".replace(regex-i:"hello", "hi")"#), @"hi World");
3712        insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi")"#), @"hi World hi");
3713        insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi", 1)"#), @"hi World Hello");
3714
3715        // replace with strings that look regex-y ($n patterns are always expanded)
3716        insta::assert_snapshot!(env.render_ok(r#"'hello\d+world'.replace('\d+', "X")"#), @"helloXworld");
3717        insta::assert_snapshot!(env.render_ok(r#""(foo)($1)bar".replace("$1", "$2")"#), @"(foo)()bar");
3718        insta::assert_snapshot!(env.render_ok(r#""test(abc)end".replace("(abc)", "X")"#), @"testXend");
3719
3720        // replace with templates
3721        insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", description.first_line())"#), @"hello description 1");
3722
3723        // replace with error
3724        insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", bad_string)"#), @"<Error: Bad>");
3725    }
3726
3727    #[test]
3728    fn test_config_value_method() {
3729        let mut env = TestTemplateEnv::new();
3730        env.add_keyword("boolean", || literal(ConfigValue::from(true)));
3731        env.add_keyword("integer", || literal(ConfigValue::from(42)));
3732        env.add_keyword("string", || literal(ConfigValue::from("foo")));
3733        env.add_keyword("string_list", || {
3734            literal(ConfigValue::from_iter(["foo", "bar"]))
3735        });
3736
3737        insta::assert_snapshot!(env.render_ok("boolean"), @"true");
3738        insta::assert_snapshot!(env.render_ok("integer"), @"42");
3739        insta::assert_snapshot!(env.render_ok("string"), @r#""foo""#);
3740        insta::assert_snapshot!(env.render_ok("string_list"), @r#"["foo", "bar"]"#);
3741
3742        insta::assert_snapshot!(env.render_ok("boolean.as_boolean()"), @"true");
3743        insta::assert_snapshot!(env.render_ok("integer.as_integer()"), @"42");
3744        insta::assert_snapshot!(env.render_ok("string.as_string()"), @"foo");
3745        insta::assert_snapshot!(env.render_ok("string_list.as_string_list()"), @"foo bar");
3746
3747        insta::assert_snapshot!(
3748            env.render_ok("boolean.as_integer()"),
3749            @"<Error: invalid type: boolean `true`, expected i64>");
3750        insta::assert_snapshot!(
3751            env.render_ok("integer.as_string()"),
3752            @"<Error: invalid type: integer `42`, expected a string>");
3753        insta::assert_snapshot!(
3754            env.render_ok("string.as_string_list()"),
3755            @r#"<Error: invalid type: string "foo", expected a sequence>"#);
3756        insta::assert_snapshot!(
3757            env.render_ok("string_list.as_boolean()"),
3758            @"<Error: invalid type: sequence, expected a boolean>");
3759    }
3760
3761    #[test]
3762    fn test_signature_and_email_methods() {
3763        let mut env = TestTemplateEnv::new();
3764
3765        env.add_keyword("author", || {
3766            literal(new_signature("Test User", "test.user@example.com"))
3767        });
3768        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
3769        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3770        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3771        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3772        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
3773        insta::assert_snapshot!(env.render_ok("author.timestamp()"), @"1970-01-01 00:00:00.000 +00:00");
3774
3775        env.add_keyword("author", || {
3776            literal(new_signature("Another Test User", "test.user@example.com"))
3777        });
3778        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
3779        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
3780        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3781
3782        env.add_keyword("author", || {
3783            literal(new_signature("Test User", "test.user@invalid@example.com"))
3784        });
3785        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
3786        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3787        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
3788        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3789        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"invalid@example.com");
3790
3791        env.add_keyword("author", || {
3792            literal(new_signature("Test User", "test.user"))
3793        });
3794        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
3795        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
3796        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3797        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
3798
3799        env.add_keyword("author", || {
3800            literal(new_signature("Test User", "test.user+tag@example.com"))
3801        });
3802        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
3803        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
3804        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user+tag");
3805        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
3806
3807        env.add_keyword("author", || literal(new_signature("Test User", "x@y")));
3808        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
3809        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
3810        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"x");
3811        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"y");
3812
3813        env.add_keyword("author", || {
3814            literal(new_signature("", "test.user@example.com"))
3815        });
3816        insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
3817        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
3818        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3819
3820        env.add_keyword("author", || literal(new_signature("Test User", "")));
3821        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
3822        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3823        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
3824        insta::assert_snapshot!(env.render_ok("author.email().local()"), @"");
3825        insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
3826
3827        env.add_keyword("author", || literal(new_signature("", "")));
3828        insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
3829        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
3830        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
3831    }
3832
3833    #[test]
3834    fn test_size_hint_method() {
3835        let mut env = TestTemplateEnv::new();
3836
3837        env.add_keyword("unbounded", || literal((5, None)));
3838        insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
3839        insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
3840        insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
3841        insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
3842
3843        env.add_keyword("bounded", || literal((0, Some(10))));
3844        insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
3845        insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
3846        insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
3847        insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
3848
3849        env.add_keyword("zero", || literal((0, Some(0))));
3850        insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
3851        insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
3852        insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
3853        insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
3854    }
3855
3856    #[test]
3857    fn test_timestamp_method() {
3858        let mut env = TestTemplateEnv::new();
3859        env.add_keyword("now", || literal(Timestamp::now()));
3860        env.add_keyword("t0", || literal(new_timestamp(0, 0)));
3861        env.add_keyword("t0_plus1", || literal(new_timestamp(0, 60)));
3862        env.add_keyword("tmax", || literal(new_timestamp(i64::MAX, 0)));
3863
3864        // Unformattable timestamp
3865        insta::assert_snapshot!(env.render_ok("tmax"),
3866            @"<Error: Out-of-range date>");
3867
3868        insta::assert_snapshot!(
3869            env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
3870            @"19700101 00:00:00");
3871
3872        // Invalid format string
3873        insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r#"
3874         --> 1:11
3875          |
3876        1 | t0.format("%_")
3877          |           ^--^
3878          |
3879          = Invalid time format
3880        "#);
3881
3882        // Invalid type
3883        insta::assert_snapshot!(env.parse_err(r#"t0.format(0)"#), @"
3884         --> 1:11
3885          |
3886        1 | t0.format(0)
3887          |           ^
3888          |
3889          = Expected string literal
3890        ");
3891
3892        // Dynamic string isn't supported yet
3893        insta::assert_snapshot!(env.parse_err(r#"t0.format("%Y" ++ "%m")"#), @r#"
3894         --> 1:11
3895          |
3896        1 | t0.format("%Y" ++ "%m")
3897          |           ^----------^
3898          |
3899          = Expected string literal
3900        "#);
3901
3902        // Literal alias expansion
3903        env.add_alias("time_format", r#""%Y-%m-%d""#);
3904        env.add_alias("bad_time_format", r#""%_""#);
3905        insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
3906        insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
3907         --> 1:11
3908          |
3909        1 | t0.format(bad_time_format)
3910          |           ^-------------^
3911          |
3912          = In alias `bad_time_format`
3913         --> 1:1
3914          |
3915        1 | "%_"
3916          | ^--^
3917          |
3918          = Invalid time format
3919        "#);
3920
3921        insta::assert_snapshot!(env.render_ok("t0_plus1.utc()"), @"1970-01-01 00:00:00.000 +00:00");
3922
3923        // TODO: exercise ago() and local() deterministically
3924        // Just make sure these methods work for now
3925        assert!(!env.render_ok("now.ago()").is_empty());
3926        assert!(!env.render_ok("now.local()").is_empty());
3927
3928        insta::assert_snapshot!(env.render_ok("t0.after('1969')"), @"true");
3929        insta::assert_snapshot!(env.render_ok("t0.before('1969')"), @"false");
3930        insta::assert_snapshot!(env.render_ok("t0.after('now')"), @"false");
3931        insta::assert_snapshot!(env.render_ok("t0.before('now')"), @"true");
3932        insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
3933         --> 1:11
3934          |
3935        1 | t0.before('invalid')
3936          |           ^-------^
3937          |
3938          = Invalid date pattern
3939        expected unsupported identifier as position 0..7
3940        ");
3941        insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
3942         --> 1:11
3943          |
3944        1 | t0.before('invalid')
3945          |           ^-------^
3946          |
3947          = Invalid date pattern
3948        expected unsupported identifier as position 0..7
3949        ");
3950
3951        // Can only compare timestamps against string literals
3952        insta::assert_snapshot!(env.parse_err("t0.after(t0)"), @"
3953         --> 1:10
3954          |
3955        1 | t0.after(t0)
3956          |          ^^
3957          |
3958          = Expected string literal
3959        ");
3960        insta::assert_snapshot!(env.parse_err("t0.before(t0)"), @"
3961         --> 1:11
3962          |
3963        1 | t0.before(t0)
3964          |           ^^
3965          |
3966          = Expected string literal
3967        ");
3968
3969        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");
3970        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");
3971        insta::assert_snapshot!(env.parse_err("t0.since(false)"), @"
3972         --> 1:10
3973          |
3974        1 | t0.since(false)
3975          |          ^---^
3976          |
3977          = Expected expression of type `Timestamp`, but actual type is `Boolean`
3978        ");
3979    }
3980
3981    #[test]
3982    fn test_timestamp_range_method() {
3983        let mut env = TestTemplateEnv::new();
3984        env.add_keyword("instant", || {
3985            literal(TimestampRange {
3986                start: new_timestamp(0, 0),
3987                end: new_timestamp(0, 0),
3988            })
3989        });
3990        env.add_keyword("one_msec", || {
3991            literal(TimestampRange {
3992                start: new_timestamp(0, 0),
3993                end: new_timestamp(1, -60),
3994            })
3995        });
3996
3997        insta::assert_snapshot!(
3998            env.render_ok("instant.start().format('%Y%m%d %H:%M:%S %Z')"),
3999            @"19700101 00:00:00 +00:00");
4000        insta::assert_snapshot!(
4001            env.render_ok("one_msec.end().format('%Y%m%d %H:%M:%S %Z')"),
4002            @"19691231 23:00:00 -01:00");
4003
4004        insta::assert_snapshot!(
4005            env.render_ok("instant.duration()"), @"less than a microsecond");
4006        insta::assert_snapshot!(
4007            env.render_ok("one_msec.duration()"), @"1 millisecond");
4008    }
4009
4010    #[test]
4011    fn test_fill_function() {
4012        let mut env = TestTemplateEnv::new();
4013        env.add_color("error", crossterm::style::Color::DarkRed);
4014
4015        insta::assert_snapshot!(
4016            env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
4017                                  label("error", "lazy") ++ " dog\n")"#),
4018            @"
4019        The quick fox jumps
4020        over the lazy dog
4021        ");
4022
4023        // A low value will not chop words, but can chop a label by words
4024        insta::assert_snapshot!(
4025            env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
4026                                  label("error", "longlonglongword and short words") ++
4027                                  " back out\n")"#),
4028            @"
4029        Longlonglongword
4030        an some
4031        short
4032        words
4033        longlonglongword
4034        and short
4035        words
4036        back out
4037        ");
4038
4039        // Filling to 0 means breaking at every word
4040        insta::assert_snapshot!(
4041            env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
4042                                  label("error", "lazy") ++ " dog\n")"#),
4043            @"
4044        The
4045        quick
4046        fox
4047        jumps
4048        over
4049        the
4050        lazy
4051        dog
4052        ");
4053
4054        // Filling to -0 is the same as 0
4055        insta::assert_snapshot!(
4056            env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
4057                                  label("error", "lazy") ++ " dog\n")"#),
4058            @"
4059        The
4060        quick
4061        fox
4062        jumps
4063        over
4064        the
4065        lazy
4066        dog
4067        ");
4068
4069        // Filling to negative width is an error
4070        insta::assert_snapshot!(
4071            env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
4072                                  label("error", "lazy") ++ " dog\n")"#),
4073            @"<Error: out of range integral type conversion attempted>");
4074
4075        // Word-wrap, then indent
4076        insta::assert_snapshot!(
4077            env.render_ok(r#""START marker to help insta\n" ++
4078                             indent("    ", fill(20, "The quick fox jumps over the " ++
4079                                                 label("error", "lazy") ++ " dog\n"))"#),
4080            @"
4081        START marker to help insta
4082            The quick fox jumps
4083            over the lazy dog
4084        ");
4085
4086        // Word-wrap indented (no special handling for leading spaces)
4087        insta::assert_snapshot!(
4088            env.render_ok(r#""START marker to help insta\n" ++
4089                             fill(20, indent("    ", "The quick fox jumps over the " ++
4090                                             label("error", "lazy") ++ " dog\n"))"#),
4091            @"
4092        START marker to help insta
4093            The quick fox
4094        jumps over the lazy
4095        dog
4096        ");
4097    }
4098
4099    #[test]
4100    fn test_indent_function() {
4101        let mut env = TestTemplateEnv::new();
4102        env.add_color("error", crossterm::style::Color::DarkRed);
4103        env.add_color("warning", crossterm::style::Color::DarkYellow);
4104        env.add_color("hint", crossterm::style::Color::DarkCyan);
4105
4106        // Empty line shouldn't be indented. Not using insta here because we test
4107        // whitespace existence.
4108        assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
4109        assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
4110        assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
4111
4112        // "\n" at end of labeled text
4113        insta::assert_snapshot!(
4114            env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
4115            @"
4116        __a
4117        __b
4118        ");
4119
4120        // "\n" in labeled text
4121        insta::assert_snapshot!(
4122            env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
4123            @"
4124        __ab
4125        __c
4126        ");
4127
4128        // Labeled prefix + unlabeled content
4129        insta::assert_snapshot!(
4130            env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
4131            @"
4132        XXa
4133        XXb
4134        ");
4135
4136        // Nested indent, silly but works
4137        insta::assert_snapshot!(
4138            env.render_ok(r#"indent(label("hint", "A"),
4139                                    label("warning", indent(label("hint", "B"),
4140                                                            label("error", "x\n") ++ "y")))"#),
4141            @"
4142        ABx
4143        ABy
4144        ");
4145    }
4146
4147    #[test]
4148    fn test_pad_function() {
4149        let mut env = TestTemplateEnv::new();
4150        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4151        env.add_color("red", crossterm::style::Color::Red);
4152        env.add_color("cyan", crossterm::style::Color::DarkCyan);
4153
4154        // Default fill_char is ' '
4155        insta::assert_snapshot!(
4156            env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
4157            @"{  foo}");
4158        insta::assert_snapshot!(
4159            env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
4160            @"{foo  }");
4161        insta::assert_snapshot!(
4162            env.render_ok(r"'{' ++ pad_centered(5, label('red', 'foo')) ++ '}'"),
4163            @"{ foo }");
4164
4165        // Labeled fill char
4166        insta::assert_snapshot!(
4167            env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4168            @"==foo");
4169        insta::assert_snapshot!(
4170            env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4171            @"foo==");
4172        insta::assert_snapshot!(
4173            env.render_ok(r"pad_centered(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4174            @"=foo=");
4175
4176        // Error in fill char: the output looks odd (because the error message
4177        // isn't 1-width character), but is still readable.
4178        insta::assert_snapshot!(
4179            env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
4180            @"foo");
4181        insta::assert_snapshot!(
4182            env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
4183            @"foo<<Error: Error: Bad>Bad>");
4184        insta::assert_snapshot!(
4185            env.render_ok(r"pad_centered(5, 'foo', fill_char=bad_string)"),
4186            @"<Error: Bad>foo<Error: Bad>");
4187
4188        // Invalid pad width is not a parse error
4189        insta::assert_snapshot!(
4190            env.render_ok("pad_start(-1, 'foo')"),
4191            @"<Error: out of range integral type conversion attempted>");
4192    }
4193
4194    #[test]
4195    fn test_hash_function() {
4196        let mut env = TestTemplateEnv::new();
4197        env.add_color("red", crossterm::style::Color::Red);
4198
4199        // hash is currently of stringified content
4200        // NOTE: hash algo and per-type behavior are not codified requirements
4201        assert_eq!(env.render_ok("hash(false)"), env.render_ok("hash('false')"));
4202        assert_eq!(env.render_ok("hash(0)"), env.render_ok("hash('0')"));
4203        assert_eq!(
4204            env.render_ok("hash(0)"),
4205            env.render_ok("hash(label('red', '0'))")
4206        );
4207    }
4208
4209    #[test]
4210    fn test_truncate_function() {
4211        let mut env = TestTemplateEnv::new();
4212        env.add_color("red", crossterm::style::Color::Red);
4213
4214        insta::assert_snapshot!(
4215            env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
4216            @"arbaz");
4217        insta::assert_snapshot!(
4218            env.render_ok("truncate_start(5, 'foo', 'bar')"), @"foo");
4219        insta::assert_snapshot!(
4220            env.render_ok("truncate_start(9, 'foobarbazquux', 'dotdot')"), @"dotdotuux");
4221
4222        insta::assert_snapshot!(
4223            env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
4224            @"fobaz");
4225        insta::assert_snapshot!(
4226            env.render_ok("truncate_end(5, 'foo', 'bar')"), @"foo");
4227        insta::assert_snapshot!(
4228            env.render_ok("truncate_end(9, 'foobarbazquux', 'dotdot')"), @"foodotdot");
4229
4230        // invalid truncate width is not a parse error
4231        insta::assert_snapshot!(
4232            env.render_ok("truncate_end(-1, 'foo')"),
4233            @"<Error: out of range integral type conversion attempted>");
4234    }
4235
4236    #[test]
4237    fn test_label_function() {
4238        let mut env = TestTemplateEnv::new();
4239        env.add_keyword("empty", || literal(true));
4240        env.add_color("error", crossterm::style::Color::DarkRed);
4241        env.add_color("warning", crossterm::style::Color::DarkYellow);
4242
4243        // Literal
4244        insta::assert_snapshot!(
4245            env.render_ok(r#"label("error", "text")"#),
4246            @"text");
4247
4248        // Evaluated property
4249        insta::assert_snapshot!(
4250            env.render_ok(r#"label("error".first_line(), "text")"#),
4251            @"text");
4252
4253        // Property evaluation error
4254        insta::assert_snapshot!(
4255            env.render_ok("label(fill(-1, 'foo'), 'text')"),
4256            @"<Error: out of range integral type conversion attempted>");
4257
4258        // Template
4259        insta::assert_snapshot!(
4260            env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
4261            @"text");
4262    }
4263
4264    #[test]
4265    fn test_raw_escape_sequence_function_strip_labels() {
4266        let mut env = TestTemplateEnv::new();
4267        env.add_color("error", crossterm::style::Color::DarkRed);
4268        env.add_color("warning", crossterm::style::Color::DarkYellow);
4269
4270        insta::assert_snapshot!(
4271            env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
4272            @"text",
4273        );
4274    }
4275
4276    #[test]
4277    fn test_raw_escape_sequence_function_ansi_escape() {
4278        let env = TestTemplateEnv::new();
4279
4280        // Sanitize ANSI escape without raw_escape_sequence
4281        insta::assert_snapshot!(env.render_ok(r#""\e""#), @"␛");
4282        insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"␛");
4283        insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"␛");
4284        insta::assert_snapshot!(
4285            env.render_ok(r#""]8;;"
4286                ++ "http://example.com"
4287                ++ "\e\\"
4288                ++ "Example"
4289                ++ "\x1b]8;;\x1B\\""#),
4290            @r"␛]8;;http://example.com␛\Example␛]8;;␛\");
4291
4292        // Don't sanitize ANSI escape with raw_escape_sequence
4293        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
4294        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
4295        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
4296        insta::assert_snapshot!(
4297            env.render_ok(r#"raw_escape_sequence("]8;;"
4298                ++ "http://example.com"
4299                ++ "\e\\"
4300                ++ "Example"
4301                ++ "\x1b]8;;\x1B\\")"#),
4302            @r"]8;;http://example.com\Example]8;;\");
4303    }
4304
4305    #[test]
4306    fn test_hyperlink_function_with_color() {
4307        let mut env = TestTemplateEnv::new();
4308        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4309        // With ColorFormatter, hyperlink emits OSC 8 escape sequences
4310        insta::assert_snapshot!(
4311            env.render_ok(r#"hyperlink("http://example.com", "Example")"#),
4312            @r"]8;;http://example.com\Example]8;;\");
4313        insta::assert_snapshot!(
4314            env.render_ok(r#"hyperlink(bad_string, "Example")"#),
4315            @"<Error: Bad>");
4316    }
4317
4318    #[test]
4319    fn test_hyperlink_function_without_color() {
4320        let env = TestTemplateEnv::new();
4321        // With PlainTextFormatter, hyperlink shows just the text
4322        insta::assert_snapshot!(
4323            env.render_plain(r#"hyperlink("http://example.com", "Example")"#),
4324            @"Example");
4325    }
4326
4327    #[test]
4328    fn test_hyperlink_function_custom_fallback() {
4329        let env = TestTemplateEnv::new();
4330        // Custom fallback is used when not outputting to color terminal
4331        insta::assert_snapshot!(
4332            env.render_plain(r#"hyperlink("http://example.com", "Example", "URL: http://example.com")"#),
4333            @"URL: http://example.com");
4334    }
4335
4336    #[test]
4337    fn test_hyperlink_function_stringify() {
4338        let env = TestTemplateEnv::new();
4339        // stringify() strips hyperlink to just text
4340        insta::assert_snapshot!(
4341            env.render_ok(r#"stringify(hyperlink("http://example.com", "Example"))"#),
4342            @"Example");
4343        // stringify then can be manipulated as plain text
4344        insta::assert_snapshot!(
4345            env.render_ok(r#"stringify(hyperlink("http://example.com", "Example")).upper()"#),
4346            @"EXAMPLE");
4347    }
4348
4349    #[test]
4350    fn test_hyperlink_function_with_separate() {
4351        let env = TestTemplateEnv::new();
4352        // separate() uses FormatRecorder internally; hyperlinks are preserved
4353        insta::assert_snapshot!(
4354            env.render_ok(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
4355            @r"]8;;http://a.com\A]8;;\ | ]8;;http://b.com\B]8;;\");
4356    }
4357
4358    #[test]
4359    fn test_hyperlink_function_with_coalesce() {
4360        let env = TestTemplateEnv::new();
4361        // coalesce() uses FormatRecorder; hyperlinks are preserved
4362        insta::assert_snapshot!(
4363            env.render_ok(r#"coalesce(hyperlink("http://example.com", "Link"), "fallback")"#),
4364            @r"]8;;http://example.com\Link]8;;\");
4365        // Falls back to second when hyperlink text is empty
4366        insta::assert_snapshot!(
4367            env.render_ok(r#"coalesce(hyperlink("http://example.com", ""), "fallback")"#),
4368            @"fallback");
4369    }
4370
4371    #[test]
4372    fn test_hyperlink_function_with_if() {
4373        let env = TestTemplateEnv::new();
4374        // if() does not use FormatRecorder; hyperlinks work directly
4375        insta::assert_snapshot!(
4376            env.render_ok(r#"if(true, hyperlink("http://example.com", "Yes"), "No")"#),
4377            @r"]8;;http://example.com\Yes]8;;\");
4378        insta::assert_snapshot!(
4379            env.render_ok(r#"if(false, "Yes", hyperlink("http://example.com", "No"))"#),
4380            @r"]8;;http://example.com\No]8;;\");
4381    }
4382
4383    #[test]
4384    fn test_hyperlink_function_plain_with_separate() {
4385        let env = TestTemplateEnv::new();
4386        // When rendering plain, hyperlinks should fall back to text
4387        insta::assert_snapshot!(
4388            env.render_plain(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
4389            @"A | B");
4390    }
4391
4392    #[test]
4393    fn test_stringify_function() {
4394        let mut env = TestTemplateEnv::new();
4395        env.add_keyword("none_i64", || literal(None::<i64>));
4396        env.add_color("error", crossterm::style::Color::DarkRed);
4397
4398        insta::assert_snapshot!(env.render_ok("stringify(false)"), @"false");
4399        insta::assert_snapshot!(env.render_ok("stringify(42).len()"), @"2");
4400        insta::assert_snapshot!(env.render_ok("stringify(none_i64)"), @"");
4401        insta::assert_snapshot!(env.render_ok("stringify(label('error', 'text'))"), @"text");
4402    }
4403
4404    #[test]
4405    fn test_json_function() {
4406        let mut env = TestTemplateEnv::new();
4407        env.add_keyword("none_i64", || literal(None::<i64>));
4408        env.add_keyword("string_list", || {
4409            literal(vec!["foo".to_owned(), "bar".to_owned()])
4410        });
4411        env.add_keyword("config_value_table", || {
4412            literal(ConfigValue::from_iter([("foo", "bar")]))
4413        });
4414        env.add_keyword("some_cfgval", || literal(Some(ConfigValue::from(1))));
4415        env.add_keyword("none_cfgval", || literal(None::<ConfigValue>));
4416        env.add_keyword("signature", || {
4417            literal(Signature {
4418                name: "Test User".to_owned(),
4419                email: "test.user@example.com".to_owned(),
4420                timestamp: Timestamp {
4421                    timestamp: MillisSinceEpoch(0),
4422                    tz_offset: 0,
4423                },
4424            })
4425        });
4426        env.add_keyword("email", || literal(Email("foo@bar".to_owned())));
4427        env.add_keyword("size_hint", || literal((5, None)));
4428        env.add_keyword("timestamp", || {
4429            literal(Timestamp {
4430                timestamp: MillisSinceEpoch(0),
4431                tz_offset: 0,
4432            })
4433        });
4434        env.add_keyword("timestamp_range", || {
4435            literal(TimestampRange {
4436                start: Timestamp {
4437                    timestamp: MillisSinceEpoch(0),
4438                    tz_offset: 0,
4439                },
4440                end: Timestamp {
4441                    timestamp: MillisSinceEpoch(86_400_000),
4442                    tz_offset: -60,
4443                },
4444            })
4445        });
4446
4447        insta::assert_snapshot!(env.render_ok(r#"json('"quoted"')"#), @r#""\"quoted\"""#);
4448        insta::assert_snapshot!(env.render_ok(r#"json(string_list)"#), @r#"["foo","bar"]"#);
4449        insta::assert_snapshot!(env.render_ok("json(false)"), @"false");
4450        insta::assert_snapshot!(env.render_ok("json(42)"), @"42");
4451        insta::assert_snapshot!(env.render_ok("json(none_i64)"), @"null");
4452        insta::assert_snapshot!(env.render_ok(r#"json(config_value_table)"#), @r#"{"foo":"bar"}"#);
4453        insta::assert_snapshot!(env.render_ok(r"json(some_cfgval)"), @"1");
4454        insta::assert_snapshot!(env.render_ok(r"json(none_cfgval)"), @"null");
4455        insta::assert_snapshot!(env.render_ok("json(email)"), @r#""foo@bar""#);
4456        insta::assert_snapshot!(
4457            env.render_ok("json(signature)"),
4458            @r#"{"name":"Test User","email":"test.user@example.com","timestamp":"1970-01-01T00:00:00Z"}"#);
4459        insta::assert_snapshot!(env.render_ok("json(size_hint)"), @"[5,null]");
4460        insta::assert_snapshot!(env.render_ok("json(timestamp)"), @r#""1970-01-01T00:00:00Z""#);
4461        insta::assert_snapshot!(
4462            env.render_ok("json(timestamp_range)"),
4463            @r#"{"start":"1970-01-01T00:00:00Z","end":"1970-01-01T23:00:00-01:00"}"#);
4464
4465        // AnyList is serializable if the inner type is.
4466        insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| s))"#), @r#"["foo","bar"]"#);
4467        insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| size_hint))"#), @"[[5,null],[5,null]]");
4468
4469        // Any is serializable if the inner types are.
4470        insta::assert_snapshot!(env.render_ok(r#"json(if(true, email, timestamp))"#), @r#""foo@bar""#);
4471        insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, config_value_table))"#), @"[5,null]");
4472
4473        // The else case missing does prevents the resulting Any expression
4474        // from being serializable.
4475        insta::assert_snapshot!(env.parse_err(r#"json(if(true, email))"#), @r###"
4476         --> 1:6
4477          |
4478        1 | json(if(true, email))
4479          |      ^-------------^
4480          |
4481          = Expected expression of type `Serialize`, but actual type is `Any`
4482        "###);
4483        insta::assert_snapshot!(env.parse_err(r#"json(if(false, email))"#), @r###"
4484         --> 1:6
4485          |
4486        1 | json(if(false, email))
4487          |      ^--------------^
4488          |
4489          = Expected expression of type `Serialize`, but actual type is `Any`
4490        "###);
4491    }
4492
4493    #[test]
4494    fn test_coalesce_function() {
4495        let mut env = TestTemplateEnv::new();
4496        env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4497        env.add_keyword("empty_string", || literal("".to_owned()));
4498        env.add_keyword("non_empty_string", || literal("a".to_owned()));
4499
4500        insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
4501        insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
4502        insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
4503        insta::assert_snapshot!(
4504            env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
4505
4506        // "false" is not empty
4507        insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
4508
4509        // Error is not empty
4510        insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
4511        // but can be short-circuited
4512        insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
4513
4514        // Keyword arguments are rejected.
4515        insta::assert_snapshot!(env.parse_err(r#"coalesce("a", value2="b")"#), @r#"
4516         --> 1:15
4517          |
4518        1 | coalesce("a", value2="b")
4519          |               ^--------^
4520          |
4521          = Function `coalesce`: Unexpected keyword arguments
4522        "#);
4523    }
4524
4525    #[test]
4526    fn test_concat_function() {
4527        let mut env = TestTemplateEnv::new();
4528        env.add_keyword("empty", || literal(true));
4529        env.add_keyword("hidden", || literal(false));
4530        env.add_color("empty", crossterm::style::Color::DarkGreen);
4531        env.add_color("error", crossterm::style::Color::DarkRed);
4532        env.add_color("warning", crossterm::style::Color::DarkYellow);
4533
4534        insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
4535        insta::assert_snapshot!(
4536            env.render_ok(r#"concat(hidden, empty)"#),
4537            @"falsetrue");
4538        insta::assert_snapshot!(
4539            env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
4540            @"ab");
4541
4542        // Keyword arguments are rejected.
4543        insta::assert_snapshot!(env.parse_err(r#"concat("a", value2="b")"#), @r#"
4544         --> 1:13
4545          |
4546        1 | concat("a", value2="b")
4547          |             ^--------^
4548          |
4549          = Function `concat`: Unexpected keyword arguments
4550        "#);
4551    }
4552
4553    #[test]
4554    fn test_join_function() {
4555        let mut env = TestTemplateEnv::new();
4556        env.add_keyword("description", || literal("".to_owned()));
4557        env.add_keyword("empty", || literal(true));
4558        env.add_keyword("hidden", || literal(false));
4559        env.add_color("empty", crossterm::style::Color::DarkGreen);
4560        env.add_color("error", crossterm::style::Color::DarkRed);
4561        env.add_color("warning", crossterm::style::Color::DarkYellow);
4562
4563        // Template literals.
4564        insta::assert_snapshot!(env.render_ok(r#"join(",")"#), @"");
4565        insta::assert_snapshot!(env.render_ok(r#"join(",", "")"#), @"");
4566        insta::assert_snapshot!(env.render_ok(r#"join(",", "a")"#), @"a");
4567        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b")"#), @"a,b");
4568        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "", "b")"#), @"a,,b");
4569        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b", "")"#), @"a,b,");
4570        insta::assert_snapshot!(env.render_ok(r#"join(",", "", "a", "b")"#), @",a,b");
4571        insta::assert_snapshot!(
4572            env.render_ok(r#"join("--", 1, "", true, "test", "")"#),
4573            @"1----true--test--");
4574
4575        // Separator is required.
4576        insta::assert_snapshot!(env.parse_err(r#"join()"#), @"
4577         --> 1:6
4578          |
4579        1 | join()
4580          |      ^
4581          |
4582          = Function `join`: Expected at least 1 arguments
4583        ");
4584
4585        // Labeled.
4586        insta::assert_snapshot!(
4587            env.render_ok(r#"join(",", label("error", ""), label("warning", "a"), "b")"#),
4588            @",a,b");
4589        insta::assert_snapshot!(
4590            env.render_ok(
4591                r#"join(label("empty", "<>"), label("error", "a"), label("warning", ""), "b")"#),
4592            @"a<><>b");
4593
4594        // List template.
4595        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ ""))"#), @"a,");
4596        insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ "b"))"#), @"a,b");
4597
4598        // Nested.
4599        insta::assert_snapshot!(
4600            env.render_ok(r#"join(",", "a", join("|", "", ""))"#), @"a,|");
4601        insta::assert_snapshot!(
4602            env.render_ok(r#"join(",", "a", join("|", "b", ""))"#), @"a,b|");
4603        insta::assert_snapshot!(
4604            env.render_ok(r#"join(",", "a", join("|", "b", "c"))"#), @"a,b|c");
4605
4606        // Keywords.
4607        insta::assert_snapshot!(
4608            env.render_ok(r#"join(",", hidden, description, empty)"#),
4609            @"false,,true");
4610        insta::assert_snapshot!(
4611            env.render_ok(r#"join(hidden, "X", "Y", "Z")"#),
4612            @"XfalseYfalseZ");
4613        insta::assert_snapshot!(
4614            env.render_ok(r#"join(hidden, empty)"#),
4615            @"true");
4616
4617        // Keyword arguments are rejected.
4618        insta::assert_snapshot!(env.parse_err(r#"join(",", "a", arg="b")"#), @r#"
4619         --> 1:16
4620          |
4621        1 | join(",", "a", arg="b")
4622          |                ^-----^
4623          |
4624          = Function `join`: Unexpected keyword arguments
4625        "#);
4626
4627        // only size hints cannot be templated / joined
4628        env.add_keyword("str_list", || {
4629            literal(vec!["foo".to_owned(), "bar".to_owned()])
4630        });
4631        env.add_keyword("none_int", || literal(None::<i64>));
4632        env.add_keyword("some_int", || literal(Some(67)));
4633        env.add_keyword("cfg_val", || {
4634            literal(ConfigValue::from_iter([("foo", "bar")]))
4635        });
4636        env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
4637        env.add_keyword("signature", || {
4638            literal(new_signature("User", "user@example.com"))
4639        });
4640        env.add_keyword("size_hint", || literal((10, None)));
4641        env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
4642        env.add_keyword("timestamp_range", || {
4643            literal(TimestampRange {
4644                start: new_timestamp(0, 0),
4645                end: new_timestamp(0, 0),
4646            })
4647        });
4648        insta::assert_snapshot!(
4649            env.render_ok("join('|', str_list, 42, none_int, some_int)"),
4650            @"foo bar|42||67");
4651        insta::assert_snapshot!(
4652            env.render_ok("join('|', cfg_val, email, signature)"),
4653            @r#"{ foo = "bar" }|me@example.com|User <user@example.com>"#);
4654        insta::assert_snapshot!(
4655            env.render_ok("join('|', timestamp, timestamp_range, str_list.map(|x| x))"),
4656            @"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");
4657        assert_matches!(
4658            env.parse_err_kind("join('|', size_hint)"),
4659            TemplateParseErrorKind::Expression(_)
4660        );
4661    }
4662
4663    #[test]
4664    fn test_separate_function() {
4665        let mut env = TestTemplateEnv::new();
4666        env.add_keyword("description", || literal("".to_owned()));
4667        env.add_keyword("empty", || literal(true));
4668        env.add_keyword("hidden", || literal(false));
4669        env.add_color("empty", crossterm::style::Color::DarkGreen);
4670        env.add_color("error", crossterm::style::Color::DarkRed);
4671        env.add_color("warning", crossterm::style::Color::DarkYellow);
4672
4673        insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
4674        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
4675        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
4676        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
4677        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
4678        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
4679        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
4680
4681        // Labeled
4682        insta::assert_snapshot!(
4683            env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
4684            @"a b");
4685
4686        // List template
4687        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
4688        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
4689
4690        // Nested separate
4691        insta::assert_snapshot!(
4692            env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
4693        insta::assert_snapshot!(
4694            env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
4695        insta::assert_snapshot!(
4696            env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
4697
4698        // Conditional template
4699        insta::assert_snapshot!(
4700            env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
4701        insta::assert_snapshot!(
4702            env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
4703        insta::assert_snapshot!(
4704            env.render_ok(r#"separate(" ", "a", if(false, "t"))"#), @"a");
4705        insta::assert_snapshot!(
4706            env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
4707        insta::assert_snapshot!(
4708            env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
4709
4710        // Separate keywords
4711        insta::assert_snapshot!(
4712            env.render_ok(r#"separate(" ", hidden, description, empty)"#),
4713            @"false true");
4714
4715        // Keyword as separator
4716        insta::assert_snapshot!(
4717            env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
4718            @"XfalseYfalseZ");
4719
4720        // Keyword arguments are rejected.
4721        insta::assert_snapshot!(env.parse_err(r#"separate(" ", "a", value2="b")"#), @r#"
4722         --> 1:20
4723          |
4724        1 | separate(" ", "a", value2="b")
4725          |                    ^--------^
4726          |
4727          = Function `separate`: Unexpected keyword arguments
4728        "#);
4729    }
4730
4731    #[test]
4732    fn test_surround_function() {
4733        let mut env = TestTemplateEnv::new();
4734        env.add_keyword("lt", || literal("<".to_owned()));
4735        env.add_keyword("gt", || literal(">".to_owned()));
4736        env.add_keyword("content", || literal("content".to_owned()));
4737        env.add_keyword("empty_content", || literal("".to_owned()));
4738        env.add_color("error", crossterm::style::Color::DarkRed);
4739        env.add_color("paren", crossterm::style::Color::Cyan);
4740
4741        insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
4742        insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
4743
4744        // Labeled
4745        insta::assert_snapshot!(
4746            env.render_ok(
4747                r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
4748            @"(a)");
4749
4750        // Keyword
4751        insta::assert_snapshot!(
4752            env.render_ok(r#"surround(lt, gt, content)"#),
4753            @"<content>");
4754        insta::assert_snapshot!(
4755            env.render_ok(r#"surround(lt, gt, empty_content)"#),
4756            @"");
4757
4758        // Conditional template as content
4759        insta::assert_snapshot!(
4760            env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
4761            @"<empty>");
4762        insta::assert_snapshot!(
4763            env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
4764            @"");
4765    }
4766
4767    #[test]
4768    fn test_config_function() {
4769        use jj_lib::config::ConfigLayer;
4770        use jj_lib::config::ConfigSource;
4771
4772        let mut config = StackedConfig::with_defaults();
4773        config
4774            .add_layer(ConfigLayer::parse(ConfigSource::User, "user.name = 'Test User'").unwrap());
4775        config.add_layer(
4776            ConfigLayer::parse(ConfigSource::User, "user.email = 'test@example.com'").unwrap(),
4777        );
4778
4779        let env = TestTemplateEnv::with_config(config);
4780
4781        // valid config path
4782        insta::assert_snapshot!(env.render_ok(r#"config("user.name")"#), @"'Test User'");
4783        insta::assert_snapshot!(env.render_ok(r#"config("user.email")"#), @"'test@example.com'");
4784        insta::assert_snapshot!(env.render_ok(r#"config("user")"#), @"{ email = 'test@example.com', name = 'Test User' }");
4785
4786        // nonexistent config path
4787        insta::assert_snapshot!(env.render_ok(r#"config("non.existent")"#), @"");
4788
4789        // conditional on config path existence
4790        insta::assert_snapshot!(env.render_ok(r#"if(config("user.name"), "yes", "no")"#), @"yes");
4791        insta::assert_snapshot!(env.render_ok(r#"if(config("non.existent"), "yes", "no")"#), @"no");
4792
4793        // malformed config path
4794        insta::assert_snapshot!(env.parse_err("config('user|name')"), @"
4795         --> 1:8
4796          |
4797        1 | config('user|name')
4798          |        ^---------^
4799          |
4800          = Failed to parse config name
4801        TOML parse error at line 1, column 5
4802          |
4803        1 | user|name
4804          |     ^
4805        invalid unquoted key, expected letters, numbers, `-`, `_`
4806        ");
4807    }
4808
4809    #[test]
4810    fn test_any_type() {
4811        let mut env = TestTemplateEnv::new();
4812        env.add_keyword("size_hint", || literal((5, None)));
4813        env.add_keyword("size_hint_2", || literal((10, None)));
4814        env.add_keyword("words", || {
4815            literal(vec!["foo".to_owned(), "bar".to_owned()])
4816        });
4817        env.add_color("red", crossterm::style::Color::Red);
4818
4819        // If requires both halves of the statement to support the trait.
4820        insta::assert_snapshot!(env.render_ok(r#"if(true, label("red", "a"), "b")"#), @"a");
4821        insta::assert_snapshot!(env.render_ok(r#"if(false, label("red", "a"), "b")"#), @"b");
4822        insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, size_hint_2))"#), @"[5,null]");
4823        insta::assert_snapshot!(env.render_ok(r#"json(if(false, size_hint, size_hint_2))"#), @"[10,null]");
4824
4825        // If one of the cases does not support Template/Serialize, fail even if
4826        // that case isn't selected.
4827        insta::assert_snapshot!(env.parse_err(r#"if(true, label("red", "a"), size_hint)"#), @r#"
4828         --> 1:1
4829          |
4830        1 | if(true, label("red", "a"), size_hint)
4831          | ^------------------------------------^
4832          |
4833          = Expected expression of type `Template`, but actual type is `Any`
4834        "#);
4835        insta::assert_snapshot!(env.parse_err(r#"json(if(true, size_hint, label("red", "a")))"#), @r#"
4836         --> 1:6
4837          |
4838        1 | json(if(true, size_hint, label("red", "a")))
4839          |      ^------------------------------------^
4840          |
4841          = Expected expression of type `Serialize`, but actual type is `Any`
4842        "#);
4843
4844        // The `join` method should not be available on `Any`.
4845        insta::assert_snapshot!(env.parse_err(r#"if(true,words,words).join(", ")"#), @r#"
4846         --> 1:22
4847          |
4848        1 | if(true,words,words).join(", ")
4849          |                      ^--^
4850          |
4851          = Method `join` doesn't exist for type `Any`
4852        "#);
4853    }
4854
4855    #[test]
4856    fn test_any_list_type() {
4857        let mut env = TestTemplateEnv::new();
4858        env.add_keyword("words", || {
4859            literal(vec!["foo".to_owned(), "bar".to_owned()])
4860        });
4861        env.add_keyword("size_hint", || literal((10, None)));
4862        env.add_color("red", crossterm::style::Color::Red);
4863
4864        // Map items are not required to implement both Template and Serialize.
4865        insta::assert_snapshot!(env.render_ok(
4866            r#"words.map(|x| label("red", x))"#),
4867            @"foo bar");
4868        insta::assert_snapshot!(env.render_ok(
4869            r#"words.map(|x| label("red", x)).join(",")"#),
4870            @"foo,bar");
4871        insta::assert_snapshot!(env.render_ok(
4872            r#"json(words.map(|x| size_hint))"#),
4873            @"[[10,null],[10,null]]");
4874
4875        // You cannot use the result if when the trait is not implemented.
4876        insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint)"#), @r#"
4877         --> 1:1
4878          |
4879        1 | words.map(|x| size_hint)
4880          | ^----------------------^
4881          |
4882          = Expected expression of type `Template`, but actual type is `AnyList`
4883        "#);
4884        insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint).join(",")"#), @r#"
4885         --> 1:26
4886          |
4887        1 | words.map(|x| size_hint).join(",")
4888          |                          ^--^
4889          |
4890          = Expected expression of type `Template`, but actual type is `AnyList`
4891        "#);
4892        insta::assert_snapshot!(env.parse_err(r#"json(words.map(|x| label("red", x)))"#), @r#"
4893         --> 1:6
4894          |
4895        1 | json(words.map(|x| label("red", x)))
4896          |      ^----------------------------^
4897          |
4898          = Expected expression of type `Serialize`, but actual type is `AnyList`
4899        "#);
4900    }
4901}