Keats / tera

A template engine for Rust based on Jinja2/Django
http://keats.github.io/tera/
MIT License
3.44k stars 280 forks source link

Inheritance breaks macro imports with "Macro namespace `macros` was not found in template `parent`" #532

Open nabijaczleweli opened 4 years ago

nabijaczleweli commented 4 years ago

Tested on 5dc12afc4e0e506183898fc56d3bdc9ce6a0b6d7 (current HEAD) and 1.3.1 off Crates.io.

Given the following setup:

{# parent #}
{% block henlo %}
henlo
{% endblock %}

{# macro #}
{% macro benlo() %}
trenlo
{% endmacro benlo %}

{# child #}
{% extends "parent" %}
{% import "macro" as macros %}

{% block henlo %}
{{ super() }}
menlo
{{ macros::benlo() }}
{% endblock %}

Running this (based on the "basic" example):

extern crate tera;
#[macro_use]
extern crate lazy_static;

use std::error::Error;
use tera::{Context, Tera};

lazy_static! {
    pub static ref TEMPLATES: Tera = {
        let mut tera = match Tera::new("templates/**/*") {
            Ok(t) => t,
            Err(e) => {
                println!("Parsing error(s): {}", e);
                ::std::process::exit(1);
            }
        };
        tera.autoescape_on(vec!["html", ".sql"]);
        tera
    };
}

fn main() {
    let context = Context::new();
    match TEMPLATES.render("child", &context) {
        Ok(s) => println!("{:?}", s),
        Err(e) => {
            println!("Error: {}", e);
            let mut cause = e.source();
            while let Some(e) = cause {
                println!("Reason: {}", e);
                cause = e.source();
            }
        }
    };
}

Yields

nabijaczleweli@tarta:~/uwu/tera/examples$ cargo run --example ekspiacja
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `/home/nabijaczleweli/uwu/tera/target/debug/examples/ekspiacja`
Error: Failed to render 'child' (error happened in 'parent').
Reason: Macro namespace `macros` was not found in template `parent`. Have you maybe forgotten to import it, or misspelled it?

which is ungood

However, removing the inheritance from child:

{# child, once again for the second time #}
{% import "macro" as macros %}

{% block henlo %}
menlo
{{ macros::benlo() }}
{% endblock %}

Yields

nabijaczleweli@tarta:~/uwu/tera/examples$ cargo run --example ekspiacja
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `/home/nabijaczleweli/uwu/tera/target/debug/examples/ekspiacja`
"\nmenlo\n\ntrenlo\n\n\n"

as expected

Keats commented 4 years ago

Sorry for the late reply. I haven't checked the cause of the error but macros are meant to be defined on separate files and not really used that way, I wouldn't be surprised some weird interactions are happening there

nabijaczleweli commented 4 years ago

Well, the examples only define macros in separate files, so that's a good start.

sooda commented 4 years ago

I think I've encountered the same bug. Can't quite make sense from the example so my case might be slightly different, but these probably have the same root cause; looks like the namespaces aren't tracked properly when the caller is using inheritance. My case has one more level of macros. The error message about a missing macro is pointing to a caller template, not the macro template. The macro files have only macros in them.

I've got page-a.html that extends base.html. In a block from the base, a macro (outer) is used from macro_a_level0.html. This macro calls a macro (inner) from macro_a_level1.html, which again calls a macro from that file (deeper).

The self::deeper() invocation fails because Tera is looking for deeper() in page_a.html.

Here's a full repository with a minimal-ish repro to help with the many files: https://github.com/sooda/bugs/tree/tera/macro-namespace-inheritance - just cargo run to see what happens. [edit: link changed a bit]

Pasting a few relevant bits below. Output:

let rendered_a = tera.render("page_a.html", &cx);
let rendered_b = tera.render("page_b.html", &cx);
// A: Err(Error { kind: Msg("Failed to render \'page_a.html\': error while rendering macro `level1::inner`"), source: Some(Error { kind: Msg("Macro `self::deeper` not found in template `page_a.html`"), source: None }) })
println!("A: {:?}", rendered_a);
// B: Ok("\n\n\n\nhello from b\n\n\n\n\n")
println!("B: {:?}", rendered_b);

Both should work, but only the B variant does. The difference between page-a.html and page-b.html is that page-b.html includes also the innermost macro file. That's an acceptable workaround for me.

$ diff -u page_a.html page_b.html
--- page_a.html 2020-08-11 22:49:06.065862910 +0300
+++ page_b.html 2020-08-11 22:49:28.049015276 +0300
@@ -1,5 +1,6 @@
 {% extends "base.html" %}
-{% import "macro_a_level0.html" as level0 %}
+{% import "macro_b_level1.html" as level1 %}
+{% import "macro_b_level0.html" as level0 %}
 {% block content %}
 {{ level0::outer() }}
 {% endblock content %}

The other files are essentially identical between a and b.

sooda commented 4 years ago

Here's another interesting repro case: multiple macro files imported from the rendered page, but no import recursion. https://github.com/sooda/bugs/tree/tera/macro-namespace-inheritance-v2

Case: page_a.html includes macro_bar.html as bar and macro_a_foo.html as foo, calls foo::outer(). outer() calls self::inner() that's in the same file. Expected: inner is found and gets rendered. Result:

Err(Error { kind: Msg("Failed to render \'page_a.html\': error while rendering macro `foo::outer`"), source: Some(Error { kind: Msg("Macro `self::inner` not found in template `page_a.html`"), source: None }) })

Changing self:: to foo:: "fixes" this: foo::inner() is obviously found in page_a.html because that page imports foo.

Deleting the bar import "fixes" this too as page_b.html in the above repo demonstrates, but what if its contents are really needed? (macro_bar.html needs to have some content for the bug to occur; this didn't repro with an empty file.)

Swapping the order of the foo and bar imports would also fix things in this particular case, but if both macro files use self::, there is no order that would work for both simultaneously. The first trick to change self to the name of the import does help though.

Keats commented 4 years ago

@sooda thanks for the debugging! Can you recreate those examples as testcases in a PR?

sooda commented 4 years ago

These occurred in normal use so there wasn't much debugging needed :) the actual bug still needs to be found and fixed.

See PR #548 for two commits. The first one is for the latter case and the second one is for the nested more complicated trouble. Not all of those tests obviously pass yet. I've add a few comments between the lines to explain the details.

Keats commented 3 years ago

Looks like the issue is that loading macro namespaces in Tera is currently a bit stupid. I have no recollection at all of that part of code so anyone will go as fast as me if they want to fix it. Thanks to @sooda there are tests for it in https://github.com/Keats/tera/pull/548

Note to myself: Tera v2 should have fewer restrictions for macros in general