Skip to content

Commit 8c8379e

Browse files
Merge pull request #649 from GuillaumeGomez/filter-lifetimes
Add support for lifetimes in filters
2 parents 02cdd4f + cfc0883 commit 8c8379e

File tree

4 files changed

+145
-51
lines changed

4 files changed

+145
-51
lines changed

askama_derive/src/filter_fn.rs

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use std::ops::ControlFlow;
77

8-
use proc_macro2::{Ident, Span, TokenStream};
8+
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
99
use quote::{ToTokens, format_ident, quote, quote_spanned};
1010
use syn::punctuated::Punctuated;
1111
use syn::spanned::Spanned;
@@ -79,19 +79,45 @@ struct FilterArgumentOptional {
7979
default: Expr,
8080
}
8181

82+
/// Internal representation for a filter function's lifetime.
83+
#[derive(Clone)]
84+
struct FilterLifetime {
85+
lifetime: Lifetime,
86+
bounds: Punctuated<Lifetime, Token![+]>,
87+
used_by_extra_args: bool,
88+
}
89+
8290
/// Internal representation for a filter function's generic argument.
8391
#[derive(Clone)]
8492
struct FilterArgumentGeneric {
8593
ident: Ident,
8694
bounds: Punctuated<TypeParamBound, Token![+]>,
8795
}
8896

97+
fn get_lifetimes(stream: TokenStream, lifetimes: &mut HashSet<Ident>) {
98+
let mut iterator = stream.into_iter().peekable();
99+
while let Some(token) = iterator.next() {
100+
match token {
101+
TokenTree::Group(g) => get_lifetimes(g.stream(), lifetimes),
102+
TokenTree::Punct(p) if p.as_char() == '\'' => {
103+
// Lifetimes are represented as `[Punct('), Ident("a")]` in the `TokenStream`.
104+
if let Some(TokenTree::Ident(i)) = iterator.peek() {
105+
lifetimes.insert(i.clone());
106+
}
107+
}
108+
TokenTree::Punct(_) | TokenTree::Ident(_) | TokenTree::Literal(_) => continue,
109+
}
110+
}
111+
}
112+
89113
/// A freestanding method annotated with `askama::filter_fn` is parsed into an instance of this
90114
/// struct, and then the resulting code is generated from there.
91115
/// This struct serves as an intermediate representation after some preprocessing on the raw AST.
92116
struct FilterSignature {
93117
/// Name of the annotated freestanding filter function
94118
ident: Ident,
119+
/// Lifetime bounds.
120+
lifetimes: Vec<FilterLifetime>,
95121
/// Name of the input variable
96122
arg_input: FilterArgumentRequired,
97123
/// Name of the askama environment variable
@@ -127,9 +153,6 @@ impl FilterSignature {
127153
if let Some(gc_arg) = sig.generics.const_params().next() {
128154
p_err!(gc_arg.span() => "Const generics are currently not supported for filters")?;
129155
}
130-
if let Some(gl_arg) = sig.generics.lifetimes().next() {
131-
p_err!(gl_arg.span() => "Lifetime generics are currently not supported for filters")?;
132-
}
133156
p_assert!(
134157
matches!(sig.output, ReturnType::Type(_, _)),
135158
sig.paren_token.span.close() => "Filter function is missing return type"
@@ -161,6 +184,7 @@ impl FilterSignature {
161184
let mut args_required = vec![];
162185
let mut args_optional = vec![];
163186
let mut args_required_generics = HashMap::default();
187+
let mut lifetimes_used_in_non_required = HashSet::default();
164188
for (arg_idx, arg) in sig.inputs.iter().skip(2).enumerate() {
165189
let FnArg::Typed(arg) = arg else {
166190
continue;
@@ -172,6 +196,7 @@ impl FilterSignature {
172196
!matches!(*arg.ty, Type::ImplTrait(_)),
173197
arg.ty.span() => "Impl generics are currently not supported for filters"
174198
)?;
199+
get_lifetimes(arg.to_token_stream(), &mut lifetimes_used_in_non_required);
175200

176201
// reference-parameters without explicit lifetime, inherit the 'filter lifetime
177202
let arg_type = patch_ref_with_lifetime(&arg.ty, &format_ident!("filter"));
@@ -220,11 +245,27 @@ impl FilterSignature {
220245
}
221246
}
222247
}
248+
// lifetimes
249+
let lifetimes = sig
250+
.generics
251+
.lifetimes()
252+
.map(|lt| {
253+
let lifetime = lt.lifetime.clone();
254+
let bounds = lt.bounds.clone();
255+
let used_by_extra_args = lifetimes_used_in_non_required.contains(&lifetime.ident);
256+
FilterLifetime {
257+
lifetime,
258+
bounds,
259+
used_by_extra_args,
260+
}
261+
})
262+
.collect::<Vec<_>>();
223263

224264
// ########################################
225265

226266
Ok(FilterSignature {
227267
ident: sig.ident.clone(),
268+
lifetimes,
228269
arg_input,
229270
arg_input_generics,
230271
arg_env,
@@ -284,6 +325,36 @@ impl FilterSignature {
284325
// code generation
285326
// ##############################################################################################
286327
impl FilterSignature {
328+
/// Returns a tuple containing two items:
329+
///
330+
/// 1. The list of lifetimes with their bounds.
331+
/// 2. The list of lifetimes without their bounds.
332+
fn lifetimes_bounds<F: Fn(&FilterLifetime) -> bool>(
333+
&self,
334+
filter: F,
335+
) -> (Vec<TokenStream>, Vec<&Lifetime>) {
336+
let mut lifetimes = Vec::with_capacity(self.lifetimes.len());
337+
let mut lifetimes_no_bounds = Vec::with_capacity(self.lifetimes.len());
338+
for lt in &self.lifetimes {
339+
if !filter(lt) {
340+
continue;
341+
}
342+
let name = &lt.lifetime;
343+
let bounds = &lt.bounds;
344+
lifetimes.push(quote! { #name: #bounds });
345+
lifetimes_no_bounds.push(name);
346+
}
347+
(lifetimes, lifetimes_no_bounds)
348+
}
349+
350+
fn lifetimes_fillers<F: Fn(&FilterLifetime) -> bool>(&self, filter: F) -> Vec<TokenStream> {
351+
self.lifetimes
352+
.iter()
353+
.filter(|l| filter(l))
354+
.map(|_| quote! { '_ })
355+
.collect()
356+
}
357+
287358
/// Generates a struct named after the filter function.
288359
/// This struct will contain all the filter's arguments (except input and env).
289360
/// The struct is basically a builder pattern for the custom filter arguments.
@@ -325,16 +396,18 @@ impl FilterSignature {
325396
let required_arg_cnt = self.args_required.len();
326397
let optional_arg_cnt = self.args_optional.len();
327398
let arg_cnt = required_arg_cnt + optional_arg_cnt;
399+
let lifetimes_fillers = self.lifetimes_fillers(|l| l.used_by_extra_args);
328400
let valid_arg_impls = (0..arg_cnt).map(|idx| {
329401
quote! {
330402
#[diagnostic::do_not_recommend]
331-
impl askama::filters::ValidArgIdx<#idx> for #ident<'_> {}
403+
impl askama::filters::ValidArgIdx<#idx> for #ident<'_, #(#lifetimes_fillers,)*> {}
332404
}
333405
});
334406

407+
let (_, lifetimes) = self.lifetimes_bounds(|l| l.used_by_extra_args);
335408
quote! {
336409
#[allow(non_camel_case_types)]
337-
#vis struct #ident<'filter, #(#struct_generics = (),)* #(const #required_flags : bool = false,)*> {
410+
#vis struct #ident<'filter, #(#lifetimes,)* #(#struct_generics = (),)* #(const #required_flags : bool = false,)*> {
338411
_lifetime: std::marker::PhantomData<&'filter ()>,
339412
/* required fields */
340413
#(#required_fields,)*
@@ -366,9 +439,10 @@ impl FilterSignature {
366439
let value = &a.default;
367440
quote! { #ident: #value }
368441
});
442+
let lifetimes_fillers = self.lifetimes_fillers(|l| l.used_by_extra_args);
369443

370444
quote! {
371-
impl std::default::Default for #ident<'_> {
445+
impl std::default::Default for #ident<'_, #(#lifetimes_fillers,)*> {
372446
fn default() -> Self {
373447
Self {
374448
_lifetime: std::marker::PhantomData::default(),
@@ -441,6 +515,7 @@ impl FilterSignature {
441515
quote! { #ident: #bounds }
442516
})
443517
.collect();
518+
let (_, lifetimes_no_bounds) = self.lifetimes_bounds(|l| l.used_by_extra_args);
444519
// return type
445520
let fn_return_ty = {
446521
let required_generics_result =
@@ -456,7 +531,7 @@ impl FilterSignature {
456531
false => format_ident!("REQUIRED_ARG_FLAG_{}", a.idx).to_token_stream(),
457532
}
458533
});
459-
quote! { #ident<'filter, #(#required_generics_result,)* #(#required_flags_result,)*> }
534+
quote! { #ident<'filter, #(#lifetimes_no_bounds,)* #(#required_generics_result,)* #(#required_flags_result,)*> }
460535
};
461536
// struct fields - (all fields, except that of current argument)
462537
let other_required_fields = self
@@ -469,8 +544,8 @@ impl FilterSignature {
469544

470545
quote! {
471546
#[allow(non_camel_case_types)]
472-
impl<'filter, #(#required_generics_impl,)* #(const #required_flags: bool,)*>
473-
#ident<'filter, #(#required_generics_impl,)* #(#required_flags,)*> {
547+
impl<'filter, #(#lifetimes_no_bounds,)* #(#required_generics_impl,)* #(const #required_flags: bool,)*>
548+
#ident<'filter, #(#lifetimes_no_bounds,)* #(#required_generics_impl,)* #(#required_flags,)*> {
474549
// named setter
475550
#[inline(always)]
476551
pub fn #named_ident<#(#required_generics_fn,)*>(self, new_value: #cur_arg_ty) -> #fn_return_ty {
@@ -530,10 +605,11 @@ impl FilterSignature {
530605
}
531606
});
532607

608+
let (_, lifetimes_no_bounds) = self.lifetimes_bounds(|l| l.used_by_extra_args);
533609
quote! {
534610
#[allow(non_camel_case_types)]
535-
impl<'filter, #(#required_generics,)* #(const #required_flags: bool,)*>
536-
#ident<'filter, #(#required_generics,)* #(#required_flags,)*> {
611+
impl<'filter, #(#lifetimes_no_bounds,)* #(#required_generics,)* #(const #required_flags: bool,)*>
612+
#ident<'filter, #(#lifetimes_no_bounds,)* #(#required_generics,)* #(#required_flags,)*> {
537613
#(#optional_setters)*
538614
}
539615
}
@@ -565,6 +641,8 @@ impl FilterSignature {
565641
let bounds = &g.bounds;
566642
quote! { #ident: #bounds }
567643
});
644+
let (all_lifetimes, _) = self.lifetimes_bounds(|_| true);
645+
let (_, type_lifetimes) = self.lifetimes_bounds(|l| l.used_by_extra_args);
568646
// env variable
569647
let env_ident = &self.arg_env.ident;
570648
let env_ty = &self.arg_env.ty;
@@ -596,13 +674,14 @@ impl FilterSignature {
596674
});
597675

598676
let impl_generics = quote! { #(#required_generics: #required_generic_bounds,)* };
599-
let impl_struct_generics = quote! { '_, #(#required_generics,)* #(#required_flags,)* };
677+
let impl_struct_generics = quote! { #(#required_generics,)* #(#required_flags,)* };
678+
let lifetimes_fillers = self.lifetimes_fillers(|l| l.used_by_extra_args);
600679
quote! {
601680
// if all required arguments have been supplied (P0 == true, P1 == true)
602681
// ... the execute() method is "unlocked":
603-
impl<#impl_generics> #ident<#impl_struct_generics> {
682+
impl<#(#all_lifetimes,)* #impl_generics> #ident<'_, #(#type_lifetimes,)* #impl_struct_generics> {
604683
#[inline(always)]
605-
pub fn execute<#(#input_bounds,)*>(self, #input_mutability #input_ident: #input_ty, #env_ident: #env_ty) #result_ty {
684+
pub fn execute< #(#input_bounds,)*>(self, #input_mutability #input_ident: #input_ty, #env_ident: #env_ty) #result_ty {
606685
// map filter variables with original name into scope
607686
#( #required_args )*
608687
#( #optional_args )*
@@ -611,7 +690,7 @@ impl FilterSignature {
611690
}
612691
}
613692

614-
impl<#impl_generics> askama::filters::ValidFilterInvocation for #ident<#impl_struct_generics> {}
693+
impl<#impl_generics> askama::filters::ValidFilterInvocation for #ident<'_, #(#lifetimes_fillers,)* #impl_struct_generics> {}
615694
}
616695
}
617696
}
@@ -626,12 +705,12 @@ fn filter_fn_impl(attr: TokenStream, ffn: &ItemFn) -> Result<TokenStream, Compil
626705

627706
let fsig = FilterSignature::try_from_signature(&ffn.sig)?;
628707

629-
let mut arg_generics = HashMap::default();
630708
for gp in &ffn.sig.generics.params {
631-
if let GenericParam::Type(gp) = gp {
632-
arg_generics.insert(gp.ident.clone(), gp.clone());
633-
} else {
634-
p_err!(gp.span() => "Only type generic arguments supported for now")?;
709+
match gp {
710+
GenericParam::Type(_) | GenericParam::Lifetime(_) => {}
711+
GenericParam::Const(_) => {
712+
p_err!(gp.span() => "Const generic arguments are not supported for now")?;
713+
}
635714
}
636715
}
637716

testing/tests/filters.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,6 @@ fn test_custom_filter_constructs() {
660660
#[test]
661661
fn filter_arguments_mutability() {
662662
mod filters {
663-
664663
// Check mutability is kept for mandatory arguments.
665664
#[askama::filter_fn]
666665
pub fn a(mut value: u32, _: &dyn askama::Values) -> askama::Result<String> {
@@ -691,3 +690,30 @@ fn filter_arguments_mutability() {
691690

692691
assert_eq!(X.render().unwrap(), "2 9 4");
693692
}
693+
694+
// Checks support for lifetimes.
695+
#[test]
696+
fn filter_lifetimes() {
697+
mod filters {
698+
use std::borrow::Cow;
699+
700+
#[askama::filter_fn]
701+
pub fn a<'a: 'b, 'b>(
702+
value: &'a str,
703+
_: &dyn askama::Values,
704+
extra: &'b str,
705+
) -> askama::Result<Cow<'a, str>> {
706+
if extra.is_empty() {
707+
Ok(Cow::Borrowed(value))
708+
} else {
709+
Ok(Cow::Owned(format!("{value}-{extra}")))
710+
}
711+
}
712+
}
713+
714+
#[derive(Template)]
715+
#[template(ext = "txt", source = r#"{{ "a"|a("b") }}"#)]
716+
struct X;
717+
718+
assert_eq!(X.render().unwrap(), "a-b");
719+
}

testing/tests/ui/filter-signature-validation.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ mod missing_required_args {
99
pub fn filter2(_: &dyn askama::Values) -> askama::Result<String> {}
1010
}
1111

12-
mod lifetime_args {
13-
#[askama::filter_fn]
14-
pub fn filter0<'a>(input: usize, _: &dyn askama::Values, arg: &'a ()) -> askama::Result<String> {}
15-
}
16-
1712
mod const_generic_args {
1813
#[askama::filter_fn]
1914
pub fn filter0<const T: bool>(input: usize, _: &dyn askama::Values) -> askama::Result<String> {}

0 commit comments

Comments
 (0)