Repository: askama-rs/askama (Rust)

Issue:
Named argument syntax in macro call expression
As followup for the macro call expression support that landed in #518:
At the moment, the macro call expression syntax only supports positioned arguments. So we currently miss support for named argument calls:

```jinja
{% macro testmacro( required_arg, optional_arg = 0 ) %}
    {{required_arg}} | {{ optional_arg }}
{% endmacro %}

{# v supported #}
{{ testmacro(42) }}

{# v supported #}
{{ testmacro(42, 1337) }}

{# v currently not supported #}
{{ testmacro(required_arg = 42, optional_arg = 1337) }}
```

At the moment, the call expr syntax "accidentally" works, because it looks like a valid rust expression. The named arguments syntax changes this symbiosis. So adding support for named arguments will probably require adding sanity checks on normal rust expressions (`doesn't contain named args`).

Would it be possible to add support for this?


Discussion:
Seems like a good idea to me. Interested into adding it? Otherwise I'll do it when I have some time.
I like the idea, too! Right now named arguments are rejected by the parser because of
<https://github.com/askama-rs/askama/blob/53ddd3cfc3208bf9e54bf41bc4356687fa68fb36/askama_parser/src/expr.rs#L305-L309>

I guess this check can be removed, if we move the logic into the generator.
> Seems like a good idea to me. Interested into adding it? Otherwise I'll do it when I have some time.

I am currently working on https://github.com/askama-rs/askama/issues/511 as an attempt of getting into the lexer code. Will let you know if it worked 😅!



Patch:
diff --git a/askama_derive/src/generator/expr.rs b/askama_derive/src/generator/expr.rs
index 093b054d1..a2134b330 100644
--- a/askama_derive/src/generator/expr.rs
+++ b/askama_derive/src/generator/expr.rs
@@ -485,6 +485,14 @@ impl<'a> Generator<'a, '_> {
         left: &WithSpan<'a, Expr<'a>>,
         args: &[WithSpan<'a, Expr<'a>>],
     ) -> Result<DisplayWrap, CompileError> {
+        // ensure that no named args are used in normal rust call expressions
+        if let Some(arg) = args
+            .iter()
+            .find(|&arg| matches!(**arg, Expr::NamedArgument(_, _)))
+        {
+            return Err(ctx.generate_error("Unsupported use of named arguments", arg.span()));
+        }
+
         match &**left {
             Expr::AssociatedItem(sub_left, AssociatedItem { name, generics })
                 if ***sub_left == Expr::Var("loop") =>
diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs
index e3c17b499..66335a8f5 100644
--- a/askama_parser/src/expr.rs
+++ b/askama_parser/src/expr.rs
@@ -251,7 +251,6 @@ impl<'a> Expr<'a> {
     pub(super) fn arguments(
         i: &mut &'a str,
         level: Level<'_>,
-        is_template_macro: bool,
     ) -> ParseResult<'a, Vec<WithSpan<'a, Self>>> {
         let _level_guard = level.nest(i)?;
         let mut named_arguments = HashSet::new();
@@ -269,15 +268,7 @@ impl<'a> Expr<'a> {
                         let has_named_arguments = !named_arguments.is_empty();
 
                         let expr = alt((
-                            move |i: &mut _| {
-                                Self::named_argument(
-                                    i,
-                                    level,
-                                    named_arguments,
-                                    start,
-                                    is_template_macro,
-                                )
-                            },
+                            move |i: &mut _| Self::named_argument(i, level, named_arguments, start),
                             move |i: &mut _| Self::parse(i, level, false),
                         ))
                         .parse_next(i)?;
@@ -300,14 +291,7 @@ impl<'a> Expr<'a> {
         level: Level<'_>,
         named_arguments: &mut HashSet<&'a str>,
         start: &'a str,
-        is_template_macro: bool,
     ) -> ParseResult<'a, WithSpan<'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 fail.parse_next(i);
-        }
-
         let (argument, _, value) = (identifier, ws('='), move |i: &mut _| {
             Self::parse(i, level, false)
         })
@@ -703,7 +687,7 @@ impl<'a> Filter<'a> {
     pub(crate) fn parse(i: &mut &'a str, level: Level<'_>) -> ParseResult<'a, Self> {
         let (name, arguments) = (
             ws(|i: &mut _| path_or_identifier(i, level)),
-            opt(|i: &mut _| Expr::arguments(i, level, true)),
+            opt(|i: &mut _| Expr::arguments(i, level)),
         )
             .parse_next(i)?;
         Ok(Self {
@@ -1126,7 +1110,7 @@ impl<'a> Suffix<'a> {
 
     fn call(i: &mut &'a str, level: Level<'_>) -> ParseResult<'a, Self> {
         (opt(|i: &mut _| call_generics(i, level)), |i: &mut _| {
-            Expr::arguments(i, level, false)
+            Expr::arguments(i, level)
         })
             .map(|(_generics, args)| Self::Call { args })
             .parse_next(i)
diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs
index 15bd26e2d..00e5b1fe6 100644
--- a/askama_parser/src/node.rs
+++ b/askama_parser/src/node.rs
@@ -885,7 +885,7 @@ impl<'a> Call<'a> {
                 (
                     opt((ws(identifier), ws("::"))),
                     ws(identifier),
-                    opt(ws(|nested: &mut _| Expr::arguments(nested, s.level, true))),
+                    opt(ws(|nested: &mut _| Expr::arguments(nested, s.level))),
                     opt(Whitespace::parse),
                     |i: &mut _| s.tag_block_end(i),
                 ),


Test patch:
diff --git a/testing/tests/macro.rs b/testing/tests/macro.rs
index 44605107d..a8192578e 100644
--- a/testing/tests/macro.rs
+++ b/testing/tests/macro.rs
@@ -580,6 +580,31 @@ fn test_expr_macro_call_importchain_nested() {
     assert_eq!(ExprMacroCall.render().unwrap(), "foo");
 }
 
+#[test]
+fn test_expr_macro_call_named_arguments() {
+    #[derive(Template)]
+    #[template(
+        source = r#"
+{%- macro testmacro(arg0 = "-", arg1 = 5) -%}
+    {{ arg0 }} | {{ arg1 }};
+{%- endmacro -%}
+{{- testmacro() }}
+{{- testmacro("a") }}
+{{- testmacro("b", 1337) }}
+{{- testmacro("c", arg1 = 1338) }}
+{{- testmacro(arg0 = "d", arg1 = 1339) -}}
+"#,
+        ext = "html"
+    )]
+    struct ExprMacroCall;
+
+    // primarily checking for compilation
+    assert_eq!(
+        ExprMacroCall.render().unwrap(),
+        "- | 5;a | 5;b | 1337;c | 1338;d | 1339;"
+    );
+}
+
 #[test]
 fn test_macro_caller_is_defined_check() {
     #[derive(Template)]
diff --git a/testing/tests/ui/caller_arguments.rs b/testing/tests/ui/caller_arguments.rs
index 6a8872b61..ad1f305db 100644
--- a/testing/tests/ui/caller_arguments.rs
+++ b/testing/tests/ui/caller_arguments.rs
@@ -89,20 +89,4 @@ struct CallerInCaller1 {
 struct JustCaller{
 }
 
-#[derive(Template)]
-#[template(
-    source = r#"
-    {% macro test() %}
-        {{ caller("a", one = "b") }}
-    {%- endmacro -%}
-    {%- call(two, one) test() -%}
-        {{- two -}} {{- one -}}
-    {%- endcall -%}
-    "#,
-    ext = "txt"
-)]
-struct NamedArguments {
-}
-
-fn main() {}
-
+fn main() {}
\ No newline at end of file
diff --git a/testing/tests/ui/caller_arguments.stderr b/testing/tests/ui/caller_arguments.stderr
index aa44e6ff4..eb7ee897c 100644
--- a/testing/tests/ui/caller_arguments.stderr
+++ b/testing/tests/ui/caller_arguments.stderr
@@ -80,18 +80,3 @@ error: block is not defined for `caller`
    |
 86 |     source = r#"{{caller()}}"#,
    |              ^^^^^^^^^^^^^^^^^
-
-error: failed to parse template source
- --> <source attribute>:3:27
-       "= \"b\") }}\n    {%- endmacro -%}\n    {%- call(two, one) test() -%}\n        {{- two"...
-   --> tests/ui/caller_arguments.rs:94:14
-    |
-94  |       source = r#"
-    |  ______________^
-95  | |     {% macro test() %}
-96  | |         {{ caller("a", one = "b") }}
-97  | |     {%- endmacro -%}
-...   |
-100 | |     {%- endcall -%}
-101 | |     "#,
-    | |______^
diff --git a/testing/tests/ui/expr_fn_calls.rs b/testing/tests/ui/expr_fn_calls.rs
new file mode 100644
index 000000000..88bac3275
--- /dev/null
+++ b/testing/tests/ui/expr_fn_calls.rs
@@ -0,0 +1,35 @@
+#![allow(unused_variables)]
+
+use askama::Template;
+
+pub fn static_fn(arg: &str) -> &str { arg }
+pub fn static_fn2(arg: &str) -> Option<&str> { Some(arg) }
+
+
+#[derive(Template)]
+#[template(source = "{{ test_fn(arg = 5) }}", ext = "html")]
+struct NamedArgsInRustExprMemberFn;
+
+impl NamedArgsInRustExprMemberFn {
+    pub fn test_fn(&self, arg: u64) -> &'static str {
+        "This is a test"
+    }
+}
+
+
+#[derive(Template)]
+#[template(source = r#"{{ self::static_fn(arg = "test") }}"#, ext = "html")]
+struct NamedArgsInRustExprStaticCall;
+
+
+#[derive(Template)]
+#[template(source = r#"{{ self::static_fn2("test").unwrap(arg = "test") }}"#, ext = "html")]
+struct NamedArgsInRustExprStaticCall2;
+
+
+#[derive(Template)]
+#[template(source = r#"{% let test = self::static_fn(arg = "test") %}"#, ext = "html")]
+struct NamedArgsInRustExprStaticCall3;
+
+
+fn main() {}
diff --git a/testing/tests/ui/expr_fn_calls.stderr b/testing/tests/ui/expr_fn_calls.stderr
new file mode 100644
index 000000000..5d7637236
--- /dev/null
+++ b/testing/tests/ui/expr_fn_calls.stderr
@@ -0,0 +1,31 @@
+error: Unsupported use of named arguments
+ --> NamedArgsInRustExprMemberFn.html:1:10
+       "(arg = 5) }}"
+  --> tests/ui/expr_fn_calls.rs:10:21
+   |
+10 | #[template(source = "{{ test_fn(arg = 5) }}", ext = "html")]
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: Unsupported use of named arguments
+ --> NamedArgsInRustExprStaticCall.html:1:18
+       "(arg = \"test\") }}"
+  --> tests/ui/expr_fn_calls.rs:21:21
+   |
+21 | #[template(source = r#"{{ self::static_fn(arg = "test") }}"#, ext = "html")]
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: Unsupported use of named arguments
+ --> NamedArgsInRustExprStaticCall2.html:1:34
+       "(arg = \"test\") }}"
+  --> tests/ui/expr_fn_calls.rs:26:21
+   |
+26 | #[template(source = r#"{{ self::static_fn2("test").unwrap(arg = "test") }}"#, ext = "html")]
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: Unsupported use of named arguments
+ --> NamedArgsInRustExprStaticCall3.html:1:29
+       "(arg = \"test\") %}"
+  --> tests/ui/expr_fn_calls.rs:31:21
+   |
+31 | #[template(source = r#"{% let test = self::static_fn(arg = "test") %}"#, ext = "html")]
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^