Repository: djc/askama
Pull request #392 (2020-12-01 14:17:34)

Improved if statement generation to avoid issues with implicit borrows
_This does not introduce any breaking changes._

Having an `if` within a `macro` is cumbersome as the parameters are referenced, requiring `if b` to become `if b.clone()`. This becomes even more cumbersome if macros call other macros, requiring various amounts of `.clone()` to be sprinkled at various places.

I changed `write_cond` to utilize Rust's automatic dereferencing rules to coerce e.g. `&bool`, `&&bool`, `&&&bool`, etc. to `bool`. This improves dealing `if b`, as now `.clone()` is no longer needed.

-----

Minimal example to highlight the issue previously:

```jinja
{% macro foo(b) %}
    {% if b %}
    {% endif %}
{% endmacro %}

{% macro bar(b) %}
    {% call foo(b) %}
{% endmacro %}

{% call foo(true) %}
{% call bar(true) %}
```

Which _essentially_ expands to:

```rust
let b = &true;
if b {}
let b = &&true;
if b {}
```

Causing compile errors as `if` expects `bool`:

```rust
error[E0308]: mismatched types
   |
22 | #[derive(Template)]
   |          ^^^^^^^^ expected `bool`, found `&bool`
error[E0308]: mismatched types
   |
22 | #[derive(Template)]
   |          ^^^^^^^^ expected `bool`, found `&&bool`
```

_I also added a new test case, to ensure this behavior persists._


Patch:
diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs
index 3a91e3c..ad086a3 100644
--- a/askama_shared/src/generator.rs
+++ b/askama_shared/src/generator.rs
@@ -518,8 +518,15 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
                         buf.dedent()?;
                         buf.write("} else if ");
                     }
+                    // The following syntax `*(&(...) as &bool)` is used to
+                    // trigger Rust's automatic dereferencing, to coerce
+                    // e.g. `&&&&&bool` to `bool`. First `&(...) as &bool`
+                    // coerces e.g. `&&&bool` to `&bool`. Then `*(&bool)`
+                    // finally dereferences it to `bool`.
+                    buf.write("*(&(");
                     let expr_code = self.visit_expr_root(expr)?;
                     buf.write(&expr_code);
+                    buf.write(") as &bool)");
                 }
                 None => {
                     buf.dedent()?;


Test patch:
diff --git a/testing/templates/if-coerce.html b/testing/templates/if-coerce.html
new file mode 100644
index 0000000..e32c2fe
--- /dev/null
+++ b/testing/templates/if-coerce.html
@@ -0,0 +1,51 @@
+{% macro foo(b) -%}
+    {% if b %}t{% else %}f{% endif -%}
+{% endmacro -%}
+
+{% macro bar(b) -%}
+    {%- call foo(b) -%}
+{% endmacro -%}
+
+{% macro baz(b) -%}
+    {%- call bar(b) -%}
+{% endmacro -%}
+
+{% macro qux(b) -%}
+    {%- call baz(b) -%}
+{% endmacro -%}
+
+{%- call foo(false) -%}
+{%- call bar(true) -%}
+{%- call baz(false) -%}
+{%- call qux(true) -%}
+
+{%- call qux(true && false) -%}
+{%- call qux(false || true) -%}
+
+{%- call qux(self.t) -%}
+{%- call qux(self.f) -%}
+{%- call qux(self.f || self.t) -%}
+
+{%- if false -%}
+if
+{%- else if false || true -%}
+elseif
+{%- else -%}
+else
+{%- endif -%}
+
+{%- if true && false -%}
+if
+{%- else if false -%}
+elseif
+{%- else -%}
+else
+{%- endif -%}
+
+{%- if false || true -%}
+if
+{%- else if (true && false) -%}
+elseif
+{%- else -%}
+else
+{%- endif -%}
diff --git a/testing/tests/coerce.rs b/testing/tests/coerce.rs
new file mode 100644
index 0000000..1a44781
--- /dev/null
+++ b/testing/tests/coerce.rs
@@ -0,0 +1,14 @@
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "if-coerce.html")]
+struct IfCoerceTemplate {
+    t: bool,
+    f: bool,
+}
+
+#[test]
+fn test_coerce() {
+    let t = IfCoerceTemplate { t: true, f: false };
+    assert_eq!(t.render().unwrap(), "ftftfttftelseifelseif");
+}