Skip to content

Commit

Permalink
Allow to pass named arguments to macro calls
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeGomez committed Nov 20, 2023
1 parent 7613cec commit 819303f
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 2 deletions.
47 changes: 47 additions & 0 deletions askama_derive/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,42 @@ impl<'a> Generator<'a> {
args.len()
)));
}
let mut output_args = Vec::new();
let args = if args
.iter()
.any(|arg| matches!(arg, Expr::NamedArgument(_, _)))
{
// First we check that all named arguments actually exist in the called item.
for arg in args.iter() {
if let Expr::NamedArgument(arg_name, _) = arg {
if !def.args.iter().any(|arg| arg == arg_name) {
return Err(CompileError::from(format!(
"no argument named `{arg_name}` in macro {name:?}"
)));
}
}
}
// We have named arguments, so we need to ensure that they are passed in the right
// order.
//
// To make things easier, we split them in two different vectors to make things simpler.
let (mut named_arguments, mut non_named_arguments): (Vec<_>, Vec<_>) = args
.iter()
.partition(|item| matches!(item, Expr::NamedArgument(_, _)));
for arg in &def.args {
if let Some(index) = named_arguments.iter().position(|item| match item {
Expr::NamedArgument(arg_name, _) => arg_name == arg,
_ => false,
}) {
output_args.push(named_arguments.remove(index).clone());
} else {
output_args.push(non_named_arguments.remove(0).clone());
}
}
&output_args
} else {
args
};
for (expr, arg) in std::iter::zip(args, &def.args) {
match expr {
// If `expr` is already a form of variable then
Expand Down Expand Up @@ -1104,6 +1140,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)?,
})
}

Expand Down Expand Up @@ -1504,6 +1541,15 @@ impl<'a> Generator<'a> {
Ok(DisplayWrap::Unwrapped)
}

fn visit_named_argument(
&mut self,
buf: &mut Buffer,
expr: &Expr<'_>,
) -> Result<DisplayWrap, CompileError> {
self.visit_expr(buf, expr)?;
Ok(DisplayWrap::Unwrapped)
}

fn visit_array(
&mut self,
buf: &mut Buffer,
Expand Down Expand Up @@ -1913,6 +1959,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,
Expand Down
37 changes: 35 additions & 2 deletions askama_parser/src/expr.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::borrow::Cow;
use std::collections::HashSet;
use std::str;

use nom::branch::alt;
Expand All @@ -12,7 +14,7 @@ use nom::sequence::{pair, preceded, terminated, tuple};
use super::{
char_lit, identifier, not_ws, num_lit, path_or_identifier, str_lit, ws, Level, PathOrIdentifier,
};
use crate::ParseResult;
use crate::{ErrorContext, ParseResult};

macro_rules! expr_prec_layer {
( $name:ident, $inner:ident, $op:expr ) => {
Expand Down Expand Up @@ -61,6 +63,7 @@ pub enum Expr<'a> {
Attr(Box<Expr<'a>>, &'a str),
Index(Box<Expr<'a>>, Box<Expr<'a>>),
Filter(&'a str, Vec<Expr<'a>>),
NamedArgument(&'a str, Box<Expr<'a>>),
Unary(&'a str, Box<Expr<'a>>),
BinOp(&'a str, Box<Expr<'a>>, Box<Expr<'a>>),
Range(&'a str, Option<Box<Expr<'a>>>, Option<Box<Expr<'a>>>),
Expand All @@ -74,15 +77,45 @@ pub enum Expr<'a> {
impl<'a> Expr<'a> {
pub(super) fn arguments(i: &'a str, level: Level) -> ParseResult<'a, Vec<Self>> {
let (_, level) = level.nest(i)?;
let mut named_arguments = HashSet::new();
let start = i;

preceded(
ws(char('(')),
cut(terminated(
separated_list0(char(','), ws(move |i| Self::parse(i, level))),
separated_list0(
char(','),
ws(alt((
move |i| Self::named_argument(i, level, &mut named_arguments, start),
move |i| Self::parse(i, level),
))),
),
char(')'),
)),
)(i)
}

pub(super) fn named_argument(
i: &'a str,
level: Level,
named_arguments: &mut HashSet<&'a str>,
start: &'a str,
) -> ParseResult<'a, Self> {
let (_, level) = level.nest(i)?;
let (i, (argument, _, value)) =
tuple((identifier, ws(char('=')), move |i| Self::parse(i, level)))(i)?;
if !named_arguments.insert(argument) {
Err(nom::Err::Failure(ErrorContext {
input: start,
message: Some(Cow::Owned(format!(
"named argument `{argument}` was passed more than once"
))),
}))
} else {
Ok((i, Self::NamedArgument(argument, Box::new(value))))
}
}

pub(super) fn parse(i: &'a str, level: Level) -> ParseResult<'a, Self> {
let (_, level) = level.nest(i)?;
let range_right = move |i| {
Expand Down

0 comments on commit 819303f

Please sign in to comment.