From b3020ee8bf979037e4191558ee7f1131b5c82de7 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 17 Nov 2023 14:39:56 +0100 Subject: Allow to pass named arguments to macro calls --- askama_derive/src/generator.rs | 56 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) (limited to 'askama_derive/src') diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index c6c5d10..65136c6 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -727,7 +727,50 @@ impl<'a> Generator<'a> { args.len() ))); } - for (expr, arg) in std::iter::zip(args, &def.args) { + let mut named_arguments = HashMap::new(); + // Since named arguments can only be passed last, we only need to check if the last argument + // is a named one. + if let Some(Expr::NamedArgument(_, _)) = args.last() { + // First we check that all named arguments actually exist in the called item. + for arg in args.iter().rev() { + let Expr::NamedArgument(arg_name, _) = arg else { + break; + }; + if !def.args.iter().any(|arg| arg == arg_name) { + return Err(CompileError::from(format!( + "no argument named `{arg_name}` in macro {name:?}" + ))); + } + named_arguments.insert(arg_name, arg); + } + } + + // Handling both named and unnamed arguments requires to be careful of the named arguments + // order. To do so, we iterate through the macro defined arguments and then check if we have + // a named argument with this name: + // + // * If there is one, we add it and move to the next argument. + // * If there isn't one, then we pick the next argument (we can do it without checking + // anything since named arguments are always last). + let mut allow_positional = true; + for (index, arg) in def.args.iter().enumerate() { + let expr = match named_arguments.get(&arg) { + Some(expr) => { + allow_positional = false; + expr + } + None => { + if !allow_positional { + // If there is already at least one named argument, then it's not allowed + // to use unnamed ones at this point anymore. + return Err(CompileError::from(format!( + "cannot have unnamed argument (`{arg}`) after named argument in macro \ + {name:?}" + ))); + } + &args[index] + } + }; match expr { // If `expr` is already a form of variable then // don't reintroduce a new variable. This is @@ -1104,6 +1147,7 @@ impl<'a> Generator<'a> { Expr::RustMacro(ref path, args) => self.visit_rust_macro(buf, path, args), Expr::Try(ref expr) => self.visit_try(buf, expr.as_ref())?, Expr::Tuple(ref exprs) => self.visit_tuple(buf, exprs)?, + Expr::NamedArgument(_, ref expr) => self.visit_named_argument(buf, expr)?, }) } @@ -1504,6 +1548,15 @@ impl<'a> Generator<'a> { Ok(DisplayWrap::Unwrapped) } + fn visit_named_argument( + &mut self, + buf: &mut Buffer, + expr: &Expr<'_>, + ) -> Result { + self.visit_expr(buf, expr)?; + Ok(DisplayWrap::Unwrapped) + } + fn visit_array( &mut self, buf: &mut Buffer, @@ -1923,6 +1976,7 @@ pub(crate) fn is_cacheable(expr: &Expr<'_>) -> bool { } Expr::Group(arg) => is_cacheable(arg), Expr::Tuple(args) => args.iter().all(is_cacheable), + Expr::NamedArgument(_, expr) => is_cacheable(expr), // We have too little information to tell if the expression is pure: Expr::Call(_, _) => false, Expr::RustMacro(_, _) => false, -- cgit