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