Skip to content

Commit

Permalink
Improve support for nested errors (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Sep 17, 2022
1 parent f9bfb4f commit 59c06c7
Show file tree
Hide file tree
Showing 35 changed files with 417 additions and 85 deletions.
2 changes: 1 addition & 1 deletion examples/error/Cargo.toml
Expand Up @@ -6,4 +6,4 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
minijinja = { path = "../../minijinja" }
minijinja = { path = "../../minijinja", features = ["internal_debug"] }
27 changes: 21 additions & 6 deletions examples/error/README.md
Expand Up @@ -5,23 +5,38 @@ template debuggability

```console
$ cargo run
Template Failed Rendering:
impossible operation: tried to use + operator on unsupported types number and string (in hello.txt:8)
Error rendering template: could not render an included template: happend in "include.txt" (in hello.txt:8)
---------------------------- Template Source -----------------------------
5 | {% with foo = 42 %}
6 | {{ range(10) }}
7 | {{ other_seq|join(" ") }}
8 > Hello {{ item_squared + bar }}!
8 > {% include "include.txt" %}
i ^^^^^^^^^^^^^^^^^^^^^ could not render an included template
9 | {% endwith %}
10 | {% endwith %}
11 | {% endfor %}
--------------------------------------------------------------------------
Referenced variables: {
bar: "test",
item_squared: 4,
other_seq: [...],
other_seq: [
0,
1,
2,
3,
4,
],
range: minijinja::functions::builtins::range,
foo: 42,
}
--------------------------------------------------------------------------

caused by: impossible operation: tried to use + operator on unsupported types number and string (in include.txt:1)
---------------------------- Template Source -----------------------------
1 > Hello {{ item_squared + bar }}!
i ^^^^^^^^^^^^^^^^^^ impossible operation
--------------------------------------------------------------------------
Referenced variables: {
bar: "test",
item_squared: 4,
}
--------------------------------------------------------------------------
```
34 changes: 20 additions & 14 deletions examples/error/src/main.rs
@@ -1,9 +1,10 @@
use minijinja::{context, Environment};

fn main() {
fn execute() -> Result<(), minijinja::Error> {
let mut env = Environment::new();
env.set_debug(true);
if let Err(err) = env.add_template(
env.add_template("include.txt", "Hello {{ item_squared + bar }}!")?;
env.add_template(
"hello.txt",
r#"
first line
Expand All @@ -12,29 +13,34 @@ fn main() {
{% with foo = 42 %}
{{ range(10) }}
{{ other_seq|join(" ") }}
Hello {{ item_squared + bar }}!
{% include "include.txt" %}
{% endwith %}
{% endwith %}
{% endfor %}
last line
"#,
) {
eprintln!("Template Failed Parsing:");
eprintln!(" {:#}", err);
std::process::exit(1);
}
)?;
let template = env.get_template("hello.txt").unwrap();
let ctx = context! {
seq => vec![2, 4, 8],
other_seq => (0..5).collect::<Vec<_>>(),
bar => "test"
};
match template.render(&ctx) {
Ok(result) => println!("{}", result),
Err(err) => {
eprintln!("Template Failed Rendering:");
eprintln!(" {:#}", err);
std::process::exit(1);
println!("{}", template.render(&ctx)?);
Ok(())
}

fn main() {
if let Err(err) = execute() {
eprintln!("Error rendering template: {:#}", err);

let mut err = &err as &dyn std::error::Error;
while let Some(next_err) = err.source() {
eprintln!();
eprintln!("caused by: {:#}", next_err);
err = next_err;
}

std::process::exit(1);
}
}
5 changes: 5 additions & 0 deletions minijinja/src/compiler/codegen.rs
Expand Up @@ -244,7 +244,9 @@ impl<'source> CodeGenerator<'source> {
ast::Stmt::Include(include) => {
self.set_line_from_span(include.span());
self.compile_expr(&include.name)?;
self.push_span(include.span());
self.add(Instruction::Include(include.ignore_missing));
self.pop_span();
}
ast::Stmt::AutoEscape(auto_escape) => {
self.set_line_from_span(auto_escape.span());
Expand Down Expand Up @@ -312,7 +314,9 @@ impl<'source> CodeGenerator<'source> {
if let ast::Expr::Call(call) = &expr.expr {
if let ast::Expr::Var(var) = &call.expr {
if var.id == "super" && call.args.is_empty() {
self.push_span(call.span());
self.add(Instruction::FastSuper);
self.pop_span();
return Ok(());
}
if var.id == "loop" && call.args.len() == 1 {
Expand Down Expand Up @@ -526,6 +530,7 @@ impl<'source> CodeGenerator<'source> {
self.sc_bool(matches!(c.op, ast::BinOpKind::ScAnd));
self.compile_expr(&c.right)?;
self.end_sc_bool();
self.pop_span();
return Ok(());
}
ast::BinOpKind::Add => Instruction::Add,
Expand Down
11 changes: 11 additions & 0 deletions minijinja/src/compiler/instructions.rs
Expand Up @@ -262,6 +262,17 @@ impl<'source> Instructions<'source> {
pub fn add_with_line(&mut self, instr: Instruction<'source>, line: usize) -> usize {
let rv = self.add(instr);
self.add_line_record(rv, line);

// if we follow up to a valid span with no more span, clear it out
#[cfg(feature = "debug")]
{
if self.span_infos.last().map_or(false, |x| x.span.is_some()) {
self.span_infos.push(SpanInfo {
first_instruction: rv as u32,
span: None,
});
}
}
rv
}

Expand Down
36 changes: 29 additions & 7 deletions minijinja/src/error.rs
Expand Up @@ -5,7 +5,12 @@ use std::fmt;
///
/// If debug mode is enabled a template error contains additional debug
/// information that can be displayed by formatting an error with the
/// alternative formatting (``format!("{:#}", err)``).
/// alternative formatting (``format!("{:#}", err)``). That information
/// is also shown for the [`Debug`] display where the extended information
/// is hidden when the alternative formatting is used.
///
/// Since MiniJinja takes advantage of chained errors it's recommended
/// to render the entire chain to better understand the causes.
///
/// # Example
///
Expand All @@ -18,8 +23,14 @@ use std::fmt;
/// match template.render(ctx) {
/// Ok(result) => println!("{}", result),
/// Err(err) => {
/// eprintln!("Could not render template:");
/// eprintln!(" {:#}", err);
/// eprintln!("Could not render template: {:#}", err);
/// // render causes as well
/// let mut err = &err as &dyn std::error::Error;
/// while let Some(next_err) = err.source() {
/// eprintln!();
/// eprintln!("caused by: {:#}", next_err);
/// err = next_err;
/// }
/// }
/// }
/// ```
Expand Down Expand Up @@ -57,10 +68,12 @@ impl fmt::Debug for Error {
// error struct dump.
#[cfg(feature = "debug")]
{
if let Some(info) = self.debug_info() {
writeln!(f)?;
render_debug_info(f, self.kind, self.line(), self.span, info)?;
writeln!(f)?;
if !f.alternate() {
if let Some(info) = self.debug_info() {
writeln!(f)?;
render_debug_info(f, self.kind, self.line(), self.span, info)?;
writeln!(f)?;
}
}
}

Expand Down Expand Up @@ -97,6 +110,12 @@ pub enum ErrorKind {
UndefinedError,
/// Impossible to serialize this value.
BadSerialization,
/// An error happened in an include.
BadInclude,
/// An error happened in a super block.
EvalBlock,
/// Unable to unpack a value.
CannotUnpack,
/// Failed writing output.
WriteFailure,
}
Expand All @@ -117,6 +136,9 @@ impl ErrorKind {
ErrorKind::BadEscape => "bad string escape",
ErrorKind::UndefinedError => "undefined value",
ErrorKind::BadSerialization => "could not serialize to internal format",
ErrorKind::BadInclude => "could not render an included template",
ErrorKind::EvalBlock => "could not render block",
ErrorKind::CannotUnpack => "cannot unpack",
ErrorKind::WriteFailure => "failed to write output",
}
}
Expand Down
2 changes: 1 addition & 1 deletion minijinja/src/value/mod.rs
Expand Up @@ -575,7 +575,7 @@ impl Value {
ValueRepr::Seq(ref v) => Ok(&v[..]),
_ => Err(Error::new(
ErrorKind::ImpossibleOperation,
"value is not a list",
format!("value of type {} is not a sequence", self.kind()),
)),
}
}
Expand Down
62 changes: 46 additions & 16 deletions minijinja/src/vm/mod.rs
Expand Up @@ -280,7 +280,27 @@ impl<'env> Vm<'env> {
state.current_block = Some(name);
if let Some(layers) = state.blocks.get(name) {
let instructions = layers.first().unwrap();
try_ctx!(self.sub_eval(state, out, instructions, state.blocks.clone()));
let referenced_template = match instructions.name() {
name if name != state.instructions.name() => Some(name),
_ => None,
};
try_ctx!(self
.sub_eval(state, out, instructions, state.blocks.clone())
.map_err(|err| {
Error::new(
ErrorKind::EvalBlock,
match referenced_template {
Some(template) => format!(
"happend in replaced block \"{}\" of \"{}\"",
name, template
),
None => {
format!("happend in local block \"{}\"", name)
}
},
)
.with_source(err)
}));
} else {
bail!(Error::new(
ErrorKind::ImpossibleOperation,
Expand Down Expand Up @@ -429,7 +449,14 @@ impl<'env> Vm<'env> {
}
let original_escape = state.auto_escape;
state.auto_escape = tmpl.initial_auto_escape();
self.sub_eval(state, out, instructions, referenced_blocks)?;
self.sub_eval(state, out, instructions, referenced_blocks)
.map_err(|err| {
Error::new(
ErrorKind::BadInclude,
format!("happend in \"{}\"", instructions.name()),
)
.with_source(err)
})?;
state.auto_escape = original_escape;
return Ok(());
}
Expand Down Expand Up @@ -476,7 +503,10 @@ impl<'env> Vm<'env> {
if capture {
out.begin_capture();
}
self.sub_eval(state, out, instructions, state.blocks.clone())?;
self.sub_eval(state, out, instructions, state.blocks.clone())
.map_err(|err| {
Error::new(ErrorKind::EvalBlock, "happend in super block").with_source(err)
})?;
if capture {
Ok(out.end_capture(state.auto_escape))
} else {
Expand Down Expand Up @@ -584,18 +614,14 @@ impl<'env> Vm<'env> {

fn unpack_list(&self, stack: &mut Stack, count: &usize) -> Result<(), Error> {
let top = stack.pop();
let v = top.as_slice().map_err(|e| {
Error::new(
ErrorKind::ImpossibleOperation,
"cannot unpack: not a sequence",
)
.with_source(e)
})?;
let v = top
.as_slice()
.map_err(|e| Error::new(ErrorKind::CannotUnpack, "not a sequence").with_source(e))?;
if v.len() != *count {
return Err(Error::new(
ErrorKind::ImpossibleOperation,
ErrorKind::CannotUnpack,
format!(
"cannot unpack: sequence of wrong length (expected {}, got {})",
"sequence of wrong length (expected {}, got {})",
*count,
v.len()
),
Expand Down Expand Up @@ -632,11 +658,15 @@ impl<'env> Vm<'env> {
}

fn process_err(mut err: Error, pc: usize, state: &State) -> Error {
if let Some(span) = state.instructions.get_span(pc) {
err.set_filename_and_span(state.instructions.name(), span);
} else if let Some(lineno) = state.instructions.get_line(pc) {
err.set_filename_and_line(state.instructions.name(), lineno);
// only attach line information if the error does not have line info yet.
if err.line().is_none() {
if let Some(span) = state.instructions.get_span(pc) {
err.set_filename_and_span(state.instructions.name(), span);
} else if let Some(lineno) = state.instructions.get_line(pc) {
err.set_filename_and_line(state.instructions.name(), lineno);
}
}
// only attach debug info if we don't have one yet and we are in debug mode.
#[cfg(feature = "debug")]
{
if state.env.debug() && err.debug_info.is_none() {
Expand Down
4 changes: 4 additions & 0 deletions minijinja/tests/inputs/err_bad_basic_block.txt
@@ -0,0 +1,4 @@
{}
---
{% extends "bad_basic_block.txt" %}
{% block title %}My Title{% endblock %}
4 changes: 4 additions & 0 deletions minijinja/tests/inputs/err_bad_block.txt
@@ -0,0 +1,4 @@
{}
---
{% extends "simple_layout.txt" %}
{% block title %}{{ missing_function() }}{% endblock %}
7 changes: 7 additions & 0 deletions minijinja/tests/inputs/err_bad_super.txt
@@ -0,0 +1,7 @@
{}
---
{% extends "bad_basic_block.txt" %}
{% block title %}My Title{% endblock %}
{% block body %}
Changed stuff goes here. {{ super() }}
{% endblock %}
5 changes: 5 additions & 0 deletions minijinja/tests/inputs/err_in_include.txt
@@ -0,0 +1,5 @@
{"seq": [1, 2, 3], "b": []}
---
{% for a in seq %}
This fails in the include: {% include "a_plus_b.txt" %}
{% endfor %}
1 change: 1 addition & 0 deletions minijinja/tests/inputs/refs/a_plus_b.txt
@@ -0,0 +1 @@
This template adds b to a: {{ a + b }}
4 changes: 4 additions & 0 deletions minijinja/tests/inputs/refs/bad_basic_block.txt
@@ -0,0 +1,4 @@
<title>{% block title %}default title{% endblock %}</title>
{% block body %}
{{ missing_function() }}
{% endblock %}
Expand Up @@ -13,11 +13,11 @@ Error {
line: 1,
}

impossible operation: cannot super outside of block (in block_super_err.txt:1)
---------------------------- Template Source -----------------------------
1 > {{ super() }}
i ^^^^^^^ impossible operation
--------------------------------------------------------------------------
Referenced variables:
--------------------------------------------------------------------------



2 changes: 2 additions & 0 deletions minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
Expand Up @@ -72,6 +72,8 @@ State {
"urlencode",
],
templates: [
"a_plus_b.txt",
"bad_basic_block.txt",
"debug.txt",
"simple_include.txt",
"simple_layout.txt",
Expand Down

0 comments on commit 59c06c7

Please sign in to comment.