Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions askama_derive/src/filter_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use proc_macro2::{Ident, Span, TokenStream};
use quote::{ToTokens, format_ident, quote, quote_spanned};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Mut;
use syn::{
Block, Expr, FnArg, GenericParam, ItemFn, Lifetime, Pat, PatType, ReturnType, Signature, Token,
Type, TypeParamBound, Visibility,
Expand Down Expand Up @@ -63,6 +64,7 @@ macro_rules! p_err {
struct FilterArgumentRequired {
idx: usize,
ident: Ident,
mutability: Option<Mut>,
ty: Type,
generics: HashSet<Ident>,
}
Expand All @@ -72,6 +74,7 @@ struct FilterArgumentRequired {
struct FilterArgumentOptional {
idx: usize,
ident: Ident,
mutability: Option<Mut>,
ty: Type,
default: Expr,
}
Expand Down Expand Up @@ -162,7 +165,7 @@ impl FilterSignature {
let FnArg::Typed(arg) = arg else {
continue;
};
let Pat::Ident(arg_name) = &*arg.pat else {
let Pat::Ident(arg_pat) = &*arg.pat else {
p_err!(arg.pat.span() => "Only conventional function arguments are supported")?
};
p_assert!(
Expand Down Expand Up @@ -190,7 +193,8 @@ impl FilterSignature {
});
args_required.push(FilterArgumentRequired {
idx: arg_idx,
ident: arg_name.ident.clone(),
ident: arg_pat.ident.clone(),
mutability: arg_pat.mutability,
ty: arg_type,
generics: used_generics,
});
Expand All @@ -208,7 +212,8 @@ impl FilterSignature {

args_optional.push(FilterArgumentOptional {
idx: arg_idx,
ident: arg_name.ident.clone(),
ident: arg_pat.ident.clone(),
mutability: arg_pat.mutability,
ty: arg_type,
default,
});
Expand Down Expand Up @@ -238,16 +243,17 @@ impl FilterSignature {
let FnArg::Typed(arg) = arg else {
p_err!(arg.span() => "Illegal or unsupported type of argument for filter function")?
};
let arg_ident = match &*arg.pat {
Pat::Ident(pat_ident) => pat_ident.ident.clone(),
Pat::Wild(pat) => Ident::new("_", pat.span()), // little hack
let (arg_ident, mutability) = match &*arg.pat {
Pat::Ident(pat_ident) => (pat_ident.ident.clone(), pat_ident.mutability),
Pat::Wild(pat) => (Ident::new("_", pat.span()), None), // little hack
_ => p_err!(arg.pat.span() => "Only conventional function arguments are supported")?,
};

Ok(FilterArgumentRequired {
idx: 0,
ident: arg_ident,
ty: *arg.ty.clone(),
mutability,
generics: generics
.keys()
.filter(|i| type_contains_ident(&arg.ty, i).is_some())
Expand Down Expand Up @@ -548,6 +554,7 @@ impl FilterSignature {
// input variable
// method generics (only the parameters not already present on struct)
let input_ident = &self.arg_input.ident;
let input_mutability = &self.arg_input.mutability;
let input_ty = &self.arg_input.ty;
let input_bounds = self
.arg_input_generics
Expand All @@ -573,8 +580,20 @@ impl FilterSignature {
// filter result
let result_ty = &self.result_ty;
// variables
let required_args = self.args_required.iter().map(|a| &a.ident);
let optional_args = self.args_optional.iter().map(|a| &a.ident);
let required_args = self.args_required.iter().map(|a| {
let mutability = a.mutability;
let ident = &a.ident;
quote! {
let #mutability #ident = unsafe { self.#ident.unwrap_unchecked() };
}
});
let optional_args = self.args_optional.iter().map(|a| {
let mutability = a.mutability;
let ident = &a.ident;
quote! {
let #mutability #ident = unsafe { self.#ident };
}
});

let impl_generics = quote! { #(#required_generics: #required_generic_bounds,)* };
let impl_struct_generics = quote! { '_, #(#required_generics,)* #(#required_flags,)* };
Expand All @@ -583,10 +602,10 @@ impl FilterSignature {
// ... the execute() method is "unlocked":
impl<#impl_generics> #ident<#impl_struct_generics> {
#[inline(always)]
pub fn execute<#(#input_bounds,)*>(self, #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
#( let #required_args = unsafe { self.#required_args.unwrap_unchecked() }; )*
#( let #optional_args = self.#optional_args; )*
#( #required_args )*
#( #optional_args )*
// insert actual filter function implementation
#filter_impl
}
Expand Down
1 change: 1 addition & 0 deletions askama_derive/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,7 @@ fn is_copyable_within_op(expr: &Expr<'_>, within_op: bool) -> bool {
| Expr::BinOp(_)
| Expr::Range(..) => true,
Expr::Unary(.., expr) => is_copyable_within_op(expr, true),
Expr::NamedArgument(_, expr) => is_copyable_within_op(expr, true),
// The result of a call likely doesn't need to be borrowed,
// as in that case the call is more likely to return a
// reference in the first place then.
Expand Down
2 changes: 1 addition & 1 deletion askama_derive/src/generator/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ impl<'a> Generator<'a, '_> {
need_borrow: bool,
) -> Result<TokenStream, CompileError> {
if let Expr::Unary(expr @ ("*" | "&"), ref arg) = ***arg {
let inner = self.visit_arg_inner(ctx, arg, ctx.span_for_node(arg.span()), true)?;
let inner = self.visit_arg_inner(ctx, arg, ctx.span_for_node(arg.span()), false)?;
let operator = TokenStream::from_str(expr).unwrap();
return Ok(quote_spanned!(span=> #operator #inner));
}
Expand Down
37 changes: 37 additions & 0 deletions testing/tests/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,3 +654,40 @@ fn test_custom_filter_constructs() {
let actual = CustomFilterConstructs {}.render().unwrap();
assert_eq!(actual, should);
}

// This test ensures that the mutability of filters arguments is kept.
// This is a regression test for <https://github.com/askama-rs/askama/issues/641>.
#[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<String> {
value += 2;
Ok(value.to_string())
}
// Check mutability is kept for extra arguments.
#[askama::filter_fn]
pub fn b(value: u32, _: &dyn askama::Values, mut other: u32) -> askama::Result<String> {
other += value;
Ok(other.to_string())
}
// Check mutability is kept for optional arguments.
#[askama::filter_fn]
pub fn c(
value: u32,
_: &dyn askama::Values,
#[optional(0)] mut other: u32,
) -> askama::Result<String> {
other += value;
Ok(other.to_string())
}
}

#[derive(Template, Debug)]
#[template(ext = "txt", source = "{{ 0|a }} {{ 7|b(2) }} {{ 1|c(other=3) }}")]
struct X;

assert_eq!(X.render().unwrap(), "2 9 4");
}