diff --git a/askama_derive/src/filter_fn.rs b/askama_derive/src/filter_fn.rs index 064c9fc0..33acfb88 100644 --- a/askama_derive/src/filter_fn.rs +++ b/askama_derive/src/filter_fn.rs @@ -5,7 +5,7 @@ use std::ops::ControlFlow; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use quote::{ToTokens, format_ident, quote, quote_spanned}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; @@ -79,6 +79,14 @@ struct FilterArgumentOptional { default: Expr, } +/// Internal representation for a filter function's lifetime. +#[derive(Clone)] +struct FilterLifetime { + lifetime: Lifetime, + bounds: Punctuated, + used_by_extra_args: bool, +} + /// Internal representation for a filter function's generic argument. #[derive(Clone)] struct FilterArgumentGeneric { @@ -86,12 +94,30 @@ struct FilterArgumentGeneric { bounds: Punctuated, } +fn get_lifetimes(stream: TokenStream, lifetimes: &mut HashSet) { + let mut iterator = stream.into_iter().peekable(); + while let Some(token) = iterator.next() { + match token { + TokenTree::Group(g) => get_lifetimes(g.stream(), lifetimes), + TokenTree::Punct(p) if p.as_char() == '\'' => { + // Lifetimes are represented as `[Punct('), Ident("a")]` in the `TokenStream`. + if let Some(TokenTree::Ident(i)) = iterator.peek() { + lifetimes.insert(i.clone()); + } + } + TokenTree::Punct(_) | TokenTree::Ident(_) | TokenTree::Literal(_) => continue, + } + } +} + /// A freestanding method annotated with `askama::filter_fn` is parsed into an instance of this /// struct, and then the resulting code is generated from there. /// This struct serves as an intermediate representation after some preprocessing on the raw AST. struct FilterSignature { /// Name of the annotated freestanding filter function ident: Ident, + /// Lifetime bounds. + lifetimes: Vec, /// Name of the input variable arg_input: FilterArgumentRequired, /// Name of the askama environment variable @@ -127,9 +153,6 @@ impl FilterSignature { if let Some(gc_arg) = sig.generics.const_params().next() { p_err!(gc_arg.span() => "Const generics are currently not supported for filters")?; } - if let Some(gl_arg) = sig.generics.lifetimes().next() { - p_err!(gl_arg.span() => "Lifetime generics are currently not supported for filters")?; - } p_assert!( matches!(sig.output, ReturnType::Type(_, _)), sig.paren_token.span.close() => "Filter function is missing return type" @@ -161,6 +184,7 @@ impl FilterSignature { let mut args_required = vec![]; let mut args_optional = vec![]; let mut args_required_generics = HashMap::default(); + let mut lifetimes_used_in_non_required = HashSet::default(); for (arg_idx, arg) in sig.inputs.iter().skip(2).enumerate() { let FnArg::Typed(arg) = arg else { continue; @@ -172,6 +196,7 @@ impl FilterSignature { !matches!(*arg.ty, Type::ImplTrait(_)), arg.ty.span() => "Impl generics are currently not supported for filters" )?; + get_lifetimes(arg.to_token_stream(), &mut lifetimes_used_in_non_required); // reference-parameters without explicit lifetime, inherit the 'filter lifetime let arg_type = patch_ref_with_lifetime(&arg.ty, &format_ident!("filter")); @@ -220,11 +245,27 @@ impl FilterSignature { } } } + // lifetimes + let lifetimes = sig + .generics + .lifetimes() + .map(|lt| { + let lifetime = lt.lifetime.clone(); + let bounds = lt.bounds.clone(); + let used_by_extra_args = lifetimes_used_in_non_required.contains(&lifetime.ident); + FilterLifetime { + lifetime, + bounds, + used_by_extra_args, + } + }) + .collect::>(); // ######################################## Ok(FilterSignature { ident: sig.ident.clone(), + lifetimes, arg_input, arg_input_generics, arg_env, @@ -284,6 +325,36 @@ impl FilterSignature { // code generation // ############################################################################################## impl FilterSignature { + /// Returns a tuple containing two items: + /// + /// 1. The list of lifetimes with their bounds. + /// 2. The list of lifetimes without their bounds. + fn lifetimes_bounds bool>( + &self, + filter: F, + ) -> (Vec, Vec<&Lifetime>) { + let mut lifetimes = Vec::with_capacity(self.lifetimes.len()); + let mut lifetimes_no_bounds = Vec::with_capacity(self.lifetimes.len()); + for lt in &self.lifetimes { + if !filter(lt) { + continue; + } + let name = <.lifetime; + let bounds = <.bounds; + lifetimes.push(quote! { #name: #bounds }); + lifetimes_no_bounds.push(name); + } + (lifetimes, lifetimes_no_bounds) + } + + fn lifetimes_fillers bool>(&self, filter: F) -> Vec { + self.lifetimes + .iter() + .filter(|l| filter(l)) + .map(|_| quote! { '_ }) + .collect() + } + /// Generates a struct named after the filter function. /// This struct will contain all the filter's arguments (except input and env). /// The struct is basically a builder pattern for the custom filter arguments. @@ -325,16 +396,18 @@ impl FilterSignature { let required_arg_cnt = self.args_required.len(); let optional_arg_cnt = self.args_optional.len(); let arg_cnt = required_arg_cnt + optional_arg_cnt; + let lifetimes_fillers = self.lifetimes_fillers(|l| l.used_by_extra_args); let valid_arg_impls = (0..arg_cnt).map(|idx| { quote! { #[diagnostic::do_not_recommend] - impl askama::filters::ValidArgIdx<#idx> for #ident<'_> {} + impl askama::filters::ValidArgIdx<#idx> for #ident<'_, #(#lifetimes_fillers,)*> {} } }); + let (_, lifetimes) = self.lifetimes_bounds(|l| l.used_by_extra_args); quote! { #[allow(non_camel_case_types)] - #vis struct #ident<'filter, #(#struct_generics = (),)* #(const #required_flags : bool = false,)*> { + #vis struct #ident<'filter, #(#lifetimes,)* #(#struct_generics = (),)* #(const #required_flags : bool = false,)*> { _lifetime: std::marker::PhantomData<&'filter ()>, /* required fields */ #(#required_fields,)* @@ -366,9 +439,10 @@ impl FilterSignature { let value = &a.default; quote! { #ident: #value } }); + let lifetimes_fillers = self.lifetimes_fillers(|l| l.used_by_extra_args); quote! { - impl std::default::Default for #ident<'_> { + impl std::default::Default for #ident<'_, #(#lifetimes_fillers,)*> { fn default() -> Self { Self { _lifetime: std::marker::PhantomData::default(), @@ -441,6 +515,7 @@ impl FilterSignature { quote! { #ident: #bounds } }) .collect(); + let (_, lifetimes_no_bounds) = self.lifetimes_bounds(|l| l.used_by_extra_args); // return type let fn_return_ty = { let required_generics_result = @@ -456,7 +531,7 @@ impl FilterSignature { false => format_ident!("REQUIRED_ARG_FLAG_{}", a.idx).to_token_stream(), } }); - quote! { #ident<'filter, #(#required_generics_result,)* #(#required_flags_result,)*> } + quote! { #ident<'filter, #(#lifetimes_no_bounds,)* #(#required_generics_result,)* #(#required_flags_result,)*> } }; // struct fields - (all fields, except that of current argument) let other_required_fields = self @@ -469,8 +544,8 @@ impl FilterSignature { quote! { #[allow(non_camel_case_types)] - impl<'filter, #(#required_generics_impl,)* #(const #required_flags: bool,)*> - #ident<'filter, #(#required_generics_impl,)* #(#required_flags,)*> { + impl<'filter, #(#lifetimes_no_bounds,)* #(#required_generics_impl,)* #(const #required_flags: bool,)*> + #ident<'filter, #(#lifetimes_no_bounds,)* #(#required_generics_impl,)* #(#required_flags,)*> { // named setter #[inline(always)] pub fn #named_ident<#(#required_generics_fn,)*>(self, new_value: #cur_arg_ty) -> #fn_return_ty { @@ -530,10 +605,11 @@ impl FilterSignature { } }); + let (_, lifetimes_no_bounds) = self.lifetimes_bounds(|l| l.used_by_extra_args); quote! { #[allow(non_camel_case_types)] - impl<'filter, #(#required_generics,)* #(const #required_flags: bool,)*> - #ident<'filter, #(#required_generics,)* #(#required_flags,)*> { + impl<'filter, #(#lifetimes_no_bounds,)* #(#required_generics,)* #(const #required_flags: bool,)*> + #ident<'filter, #(#lifetimes_no_bounds,)* #(#required_generics,)* #(#required_flags,)*> { #(#optional_setters)* } } @@ -565,6 +641,8 @@ impl FilterSignature { let bounds = &g.bounds; quote! { #ident: #bounds } }); + let (all_lifetimes, _) = self.lifetimes_bounds(|_| true); + let (_, type_lifetimes) = self.lifetimes_bounds(|l| l.used_by_extra_args); // env variable let env_ident = &self.arg_env.ident; let env_ty = &self.arg_env.ty; @@ -596,13 +674,14 @@ impl FilterSignature { }); let impl_generics = quote! { #(#required_generics: #required_generic_bounds,)* }; - let impl_struct_generics = quote! { '_, #(#required_generics,)* #(#required_flags,)* }; + let impl_struct_generics = quote! { #(#required_generics,)* #(#required_flags,)* }; + let lifetimes_fillers = self.lifetimes_fillers(|l| l.used_by_extra_args); quote! { // if all required arguments have been supplied (P0 == true, P1 == true) // ... the execute() method is "unlocked": - impl<#impl_generics> #ident<#impl_struct_generics> { + impl<#(#all_lifetimes,)* #impl_generics> #ident<'_, #(#type_lifetimes,)* #impl_struct_generics> { #[inline(always)] - pub fn execute<#(#input_bounds,)*>(self, #input_mutability #input_ident: #input_ty, #env_ident: #env_ty) #result_ty { + pub fn execute< #(#input_bounds,)*>(self, #input_mutability #input_ident: #input_ty, #env_ident: #env_ty) #result_ty { // map filter variables with original name into scope #( #required_args )* #( #optional_args )* @@ -611,7 +690,7 @@ impl FilterSignature { } } - impl<#impl_generics> askama::filters::ValidFilterInvocation for #ident<#impl_struct_generics> {} + impl<#impl_generics> askama::filters::ValidFilterInvocation for #ident<'_, #(#lifetimes_fillers,)* #impl_struct_generics> {} } } } @@ -626,12 +705,12 @@ fn filter_fn_impl(attr: TokenStream, ffn: &ItemFn) -> Result "Only type generic arguments supported for now")?; + match gp { + GenericParam::Type(_) | GenericParam::Lifetime(_) => {} + GenericParam::Const(_) => { + p_err!(gp.span() => "Const generic arguments are not supported for now")?; + } } } diff --git a/testing/tests/filters.rs b/testing/tests/filters.rs index 0e9dbaa2..63c1d186 100644 --- a/testing/tests/filters.rs +++ b/testing/tests/filters.rs @@ -660,7 +660,6 @@ fn test_custom_filter_constructs() { #[test] fn filter_arguments_mutability() { mod filters { - // Check mutability is kept for mandatory arguments. #[askama::filter_fn] pub fn a(mut value: u32, _: &dyn askama::Values) -> askama::Result { @@ -691,3 +690,30 @@ fn filter_arguments_mutability() { assert_eq!(X.render().unwrap(), "2 9 4"); } + +// Checks support for lifetimes. +#[test] +fn filter_lifetimes() { + mod filters { + use std::borrow::Cow; + + #[askama::filter_fn] + pub fn a<'a: 'b, 'b>( + value: &'a str, + _: &dyn askama::Values, + extra: &'b str, + ) -> askama::Result> { + if extra.is_empty() { + Ok(Cow::Borrowed(value)) + } else { + Ok(Cow::Owned(format!("{value}-{extra}"))) + } + } + } + + #[derive(Template)] + #[template(ext = "txt", source = r#"{{ "a"|a("b") }}"#)] + struct X; + + assert_eq!(X.render().unwrap(), "a-b"); +} diff --git a/testing/tests/ui/filter-signature-validation.rs b/testing/tests/ui/filter-signature-validation.rs index fe593d7e..6099ad62 100644 --- a/testing/tests/ui/filter-signature-validation.rs +++ b/testing/tests/ui/filter-signature-validation.rs @@ -9,11 +9,6 @@ mod missing_required_args { pub fn filter2(_: &dyn askama::Values) -> askama::Result {} } -mod lifetime_args { - #[askama::filter_fn] - pub fn filter0<'a>(input: usize, _: &dyn askama::Values, arg: &'a ()) -> askama::Result {} -} - mod const_generic_args { #[askama::filter_fn] pub fn filter0(input: usize, _: &dyn askama::Values) -> askama::Result {} diff --git a/testing/tests/ui/filter-signature-validation.stderr b/testing/tests/ui/filter-signature-validation.stderr index 0a78387c..e99d7678 100644 --- a/testing/tests/ui/filter-signature-validation.stderr +++ b/testing/tests/ui/filter-signature-validation.stderr @@ -16,62 +16,56 @@ error: Filter function missing required environment argument. Example: `fn filte 9 | pub fn filter2(_: &dyn askama::Values) -> askama::Result {} | ^ -error: Lifetime generics are currently not supported for filters - --> tests/ui/filter-signature-validation.rs:14:20 - | -14 | pub fn filter0<'a>(input: usize, _: &dyn askama::Values, arg: &'a ()) -> askama::Result {} - | ^^ - error: Const generics are currently not supported for filters - --> tests/ui/filter-signature-validation.rs:19:20 + --> tests/ui/filter-signature-validation.rs:14:20 | -19 | pub fn filter0(input: usize, _: &dyn askama::Values) -> askama::Result {} +14 | pub fn filter0(input: usize, _: &dyn askama::Values) -> askama::Result {} | ^^^^^ error: Filter functions don't support generic parameter defaults - --> tests/ui/filter-signature-validation.rs:24:24 + --> tests/ui/filter-signature-validation.rs:19:24 | -24 | pub fn filter0(input: usize, _: &dyn askama::Values, arg: T) -> askama::Result {} +19 | pub fn filter0(input: usize, _: &dyn askama::Values, arg: T) -> askama::Result {} | ^^^ error: Filter function is missing return type - --> tests/ui/filter-signature-validation.rs:29:56 + --> tests/ui/filter-signature-validation.rs:24:56 | -29 | pub fn filter0(input: usize, _: &dyn askama::Values) {} +24 | pub fn filter0(input: usize, _: &dyn askama::Values) {} | ^ error: Only conventional function arguments are supported - --> tests/ui/filter-signature-validation.rs:35:58 + --> tests/ui/filter-signature-validation.rs:30:58 | -35 | pub fn filter0(input: usize, _: &dyn askama::Values, Wrapper(arg): Wrapper) -> askama::Result {} +30 | pub fn filter0(input: usize, _: &dyn askama::Values, Wrapper(arg): Wrapper) -> askama::Result {} | ^^^^^^^ error: Impl generics are currently not supported for filters - --> tests/ui/filter-signature-validation.rs:40:63 + --> tests/ui/filter-signature-validation.rs:35:63 | -40 | pub fn filter0(input: usize, _: &dyn askama::Values, arg: impl std::fmt::Display) -> askama::Result {} +35 | pub fn filter0(input: usize, _: &dyn askama::Values, arg: impl std::fmt::Display) -> askama::Result {} | ^^^^ error: All required arguments must appear before any optional ones - --> tests/ui/filter-signature-validation.rs:45:92 + --> tests/ui/filter-signature-validation.rs:40:92 | -45 | pub fn filter0(input: usize, _: &dyn askama::Values, #[optional(1337)] opt_arg: usize, req_arg: usize) -> askama::Result {} +40 | pub fn filter0(input: usize, _: &dyn askama::Values, #[optional(1337)] opt_arg: usize, req_arg: usize) -> askama::Result {} | ^^^^^^^ error: Impl generics are currently not supported for filters - --> tests/ui/filter-signature-validation.rs:50:85 + --> tests/ui/filter-signature-validation.rs:45:85 | -50 | pub fn filter0(input: usize, _: &dyn askama::Values, #[optional(1337)] opt_arg: impl std::fmt::Display) -> askama::Result {} +45 | pub fn filter0(input: usize, _: &dyn askama::Values, #[optional(1337)] opt_arg: impl std::fmt::Display) -> askama::Result {} | ^^^^ error: Optional arguments must not use generic parameters - --> tests/ui/filter-signature-validation.rs:53:99 + --> tests/ui/filter-signature-validation.rs:48:99 | -53 | pub fn filter1(input: usize, _: &dyn askama::Values, #[optional(1337usize)] opt_arg: T) -> askama::Result {} +48 | pub fn filter1(input: usize, _: &dyn askama::Values, #[optional(1337usize)] opt_arg: T) -> askama::Result {} | ^ error: Optional arguments must not use generic parameters - --> tests/ui/filter-signature-validation.rs:56:106 + --> tests/ui/filter-signature-validation.rs:51:106 | -56 | pub fn filter2(input: usize, _: &dyn askama::Values, #[optional(1337usize)] opt_arg: Option) -> askama::Result {} +51 | pub fn filter2(input: usize, _: &dyn askama::Values, #[optional(1337usize)] opt_arg: Option) -> askama::Result {} | ^