Repository: rinja-rs/rinja (rust)

Issue:
macros fail on invocation when arguments are rust keywords
**I am using git main**

I can define this macro without issue

```handlebars
{% macro foo(type="text") %}
    <input type="{{ type }}"/>
{% endmacro foo %}

{# or this one #}

{% macro foo(type) %}
    <input type="{{ type }}"/>
{% endmacro foo %}
```
But calling them produces an error (error shown below).
```handlebars
{% call foo("input") %}
{% call foo(type="input") %}
```

```
error: expected identifier, found keyword `type`
  --> [..path]
   |
30 | #[derive(Template, Debug)]
   |          ^^^^^^^^ expected identifier, found keyword
   |
   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)

error: proc-macro derive produced unparsable tokens
  --> [..path]
   |
30 | #[derive(Template, Debug)]
   |          ^^^^^^^^

error[E0609]: no field `r#type` on type `&[Struct with Template derived]`
  --> [..path]
   |
30 | #[derive(Template, Debug)]
   |          ^^^^^^^^ unknown field
   |
   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)

```

Did not see any issues created in regards to this, or mention in the docs that it should be avoided. Just wanted to point it out in case it does not align with intentions.

As a user, I feel the error should at least come from the definition, and not the invocation. It does seem like the keyword is properly escaped in at least some capacity (the error references `r#type`).

Current workaround, naming the argument something else

Pull request description:
fix: normalize macro args on call (fixes #259)
I believe this should resolve #259.

Is it correct that `this.locals` should store only normalized identifiers, since there is some assumption that they must be written (and thus valid rust)?

Please feel free to correct me if that is not the case. 

Edit: there is also a failing ui test that seems unrelated. Can dig into it deeper if needed, but must return to the day job for now

Patch:
diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs
index 4161e664..f512f61a 100644
--- a/rinja_derive/src/generator.rs
+++ b/rinja_derive/src/generator.rs
@@ -848,8 +848,11 @@ impl<'a> Generator<'a> {
                             ("", "")
                         };
                         value.write(this.visit_expr_root(ctx, expr)?);
-                        buf.write(format_args!("let {arg} = {before}{value}{after};"));
-                        this.locals.insert_with_default(Cow::Borrowed(arg));
+                        // We need to normalize the arg to write it, thus we need to add it to
+                        // locals in the normalized manner
+                        let normalized_arg = normalize_identifier(arg);
+                        buf.write(format_args!("let {} = {before}{value}{after};", normalized_arg));
+                        this.locals.insert_with_default(Cow::Borrowed(normalized_arg));
                     }
                 }
             }


Test patch:
diff --git a/testing/tests/macro.rs b/testing/tests/macro.rs
index d977826e..9a4b2247 100644
--- a/testing/tests/macro.rs
+++ b/testing/tests/macro.rs
@@ -282,3 +282,79 @@ fn test_default_value5() {
 
     assert_eq!(MacroDefaultValue5.render().unwrap(), "3 3\n1 1\n1 2\n");
 }
+
+// This test ensures...
+// * macro arguments can be named as rust keywords (e.g. "type", "as", "ref", etc.).
+// * the keyword is escaped at definition.
+// * the keyword is escaped at invocation.
+#[test]
+fn test_rust_keywords_as_args() {
+    #[derive(Template)]
+    #[template(
+        source = "
+{%- macro input(type) -%}
+{{ type }}
+{% endmacro -%}
+
+{%- call input(1) -%}
+{%- call input(type=1) -%}
+",
+        ext = "html"
+    )]
+    struct MacroRustKwArgs;
+
+    // primarily checking for compilation
+    assert_eq!(MacroRustKwArgs.render().unwrap(), "1\n1\n");
+}
+
+// This test ensures...
+// * macro arguments can be named as rust keywords (e.g. "type", "as", "ref", etc.).
+// * the argument has a default value.
+// * the keyword is escaped at definition.
+// * the keyword is escaped at invocation.
+#[test]
+fn test_rust_keywords_as_args_with_default() {
+    #[derive(Template)]
+    #[template(
+        source = "
+{%- macro input(type=1) -%}
+{{ type }}
+{% endmacro -%}
+
+{%- call input() -%}
+{%- call input(1) -%}
+{%- call input(type=1) -%}
+",
+        ext = "html"
+    )]
+    struct MacroRustKwArgsDefault;
+
+    // primarily checking for compilation
+    assert_eq!(MacroRustKwArgsDefault.render().unwrap(), "1\n1\n1\n");
+}
+
+// This test ensures...
+// * macro arguments can be named as rust keywords (e.g. "type", "as", "ref", etc.).
+// * a non-literal expression is given as default
+// * the keyword is escaped at definition.
+// * the keyword is escaped at invocation.
+#[test]
+fn test_rust_keywords_as_args_with_default_expr() {
+    #[derive(Template)]
+    #[template(
+        source = "
+{%- macro input(type=1+2) -%}
+{{ type }}
+{% endmacro -%}
+
+{%- call input() -%}
+{%- call input(1) -%}
+{%- call input(type=1) -%}
+",
+        ext = "html"
+    )]
+    struct MacroRustKwArgsDefaultExpr;
+
+    // primarily checking for compilation
+    assert_eq!(MacroRustKwArgsDefaultExpr.render().unwrap(), "3\n1\n1\n");
+}