aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Guillaume Gomez <guillaume1.gomez@gmail.com>2023-11-17 14:39:56 +0100
committerLibravatar Dirkjan Ochtman <dirkjan@ochtman.nl>2023-11-28 11:36:09 +0100
commitb3020ee8bf979037e4191558ee7f1131b5c82de7 (patch)
tree17e82452314d507f105f6030cc8949d149a4ff5f
parent80238d7f48fd86ef939e74df9fdc9678ee78a208 (diff)
downloadaskama-b3020ee8bf979037e4191558ee7f1131b5c82de7.tar.gz
askama-b3020ee8bf979037e4191558ee7f1131b5c82de7.tar.bz2
askama-b3020ee8bf979037e4191558ee7f1131b5c82de7.zip
Allow to pass named arguments to macro calls
-rw-r--r--askama_derive/src/generator.rs56
-rw-r--r--askama_parser/src/expr.rs79
-rw-r--r--askama_parser/src/node.rs2
3 files changed, 130 insertions, 7 deletions
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<DisplayWrap, CompileError> {
+ 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,
diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs
index 76691be..2928b57 100644
--- a/askama_parser/src/expr.rs
+++ b/askama_parser/src/expr.rs
@@ -1,3 +1,5 @@
+use std::borrow::Cow;
+use std::collections::HashSet;
use std::str;
use nom::branch::alt;
@@ -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 ) => {
@@ -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>>>),
@@ -72,17 +75,83 @@ pub enum Expr<'a> {
}
impl<'a> Expr<'a> {
- pub(super) fn arguments(i: &'a str, level: Level) -> ParseResult<'a, Vec<Self>> {
+ pub(super) fn arguments(
+ i: &'a str,
+ level: Level,
+ is_template_macro: bool,
+ ) -> 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(move |i| {
+ // Needed to prevent borrowing it twice between this closure and the one
+ // calling `Self::named_arguments`.
+ let named_arguments = &mut named_arguments;
+ let has_named_arguments = !named_arguments.is_empty();
+
+ let (i, expr) = alt((
+ move |i| {
+ Self::named_argument(
+ i,
+ level,
+ named_arguments,
+ start,
+ is_template_macro,
+ )
+ },
+ move |i| Self::parse(i, level),
+ ))(i)?;
+ if has_named_arguments && !matches!(expr, Self::NamedArgument(_, _)) {
+ Err(nom::Err::Failure(ErrorContext {
+ input: start,
+ message: Some(Cow::Borrowed(
+ "named arguments must always be passed last",
+ )),
+ }))
+ } else {
+ Ok((i, expr))
+ }
+ }),
+ ),
char(')'),
)),
)(i)
}
+ fn named_argument(
+ i: &'a str,
+ level: Level,
+ named_arguments: &mut HashSet<&'a str>,
+ start: &'a str,
+ is_template_macro: bool,
+ ) -> ParseResult<'a, Self> {
+ if !is_template_macro {
+ // If this is not a template macro, we don't want to parse named arguments so
+ // we instead return an error which will allow to continue the parsing.
+ return Err(nom::Err::Error(error_position!(i, ErrorKind::Alt)));
+ }
+
+ 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) {
+ Ok((i, Self::NamedArgument(argument, Box::new(value))))
+ } else {
+ Err(nom::Err::Failure(ErrorContext {
+ input: start,
+ message: Some(Cow::Owned(format!(
+ "named argument `{argument}` was passed more than once"
+ ))),
+ }))
+ }
+ }
+
pub(super) fn parse(i: &'a str, level: Level) -> ParseResult<'a, Self> {
let (_, level) = level.nest(i)?;
let range_right = move |i| {
@@ -122,7 +191,7 @@ impl<'a> Expr<'a> {
let (i, (_, fname, args)) = tuple((
char('|'),
ws(identifier),
- opt(|i| Expr::arguments(i, level)),
+ opt(|i| Expr::arguments(i, level, false)),
))(i)?;
Ok((i, (fname, args)))
}
@@ -354,7 +423,7 @@ impl<'a> Suffix<'a> {
fn call(i: &'a str, level: Level) -> ParseResult<'a, Self> {
let (_, level) = level.nest(i)?;
- map(move |i| Expr::arguments(i, level), Self::Call)(i)
+ map(move |i| Expr::arguments(i, level, false), Self::Call)(i)
}
fn r#try(i: &'a str) -> ParseResult<'a, Self> {
diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs
index ba117c5..d11895b 100644
--- a/askama_parser/src/node.rs
+++ b/askama_parser/src/node.rs
@@ -577,7 +577,7 @@ impl<'a> Call<'a> {
cut(tuple((
opt(tuple((ws(identifier), ws(tag("::"))))),
ws(identifier),
- opt(ws(|nested| Expr::arguments(nested, s.level.get()))),
+ opt(ws(|nested| Expr::arguments(nested, s.level.get(), true))),
opt(Whitespace::parse),
))),
));