Skip to content

Commit

Permalink
feat(errors): Report context during render
Browse files Browse the repository at this point in the history
Fixes #105, mostly.  You get enough context.  This is also more
universal (hard to track line numbers in every context).  Any further
improvements will be a part of #138.

This locks mostly locks down the error API, minus `FilterError` which
won't happen until filters get refactored.  See #114.
  • Loading branch information
epage committed Jan 20, 2018
1 parent 932efd4 commit 73e26cf
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 69 deletions.
10 changes: 5 additions & 5 deletions src/compiler/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub fn unexpected_token_error<S: ToString>(expected: &str, actual: Option<S>) ->

pub fn unexpected_token_error_string(expected: &str, actual: Option<String>) -> Error {
let actual = actual.unwrap_or_else(|| "nothing".to_owned());
Error::with_msg(format!("Expected {}, found {}", expected, actual))
Error::with_msg(format!("Expected {}, found `{}`", expected, actual))
}

// creates an expression, which wraps everything that gets rendered
Expand Down Expand Up @@ -230,7 +230,7 @@ pub fn expect<'a, T>(tokens: &mut T, expected: &Token) -> Result<&'a Token>
{
match tokens.next() {
Some(x) if x == expected => Ok(x),
x => Err(unexpected_token_error(&expected.to_string(), x)),
x => Err(unexpected_token_error(&format!("`{}`", expected), x)),
}
}

Expand Down Expand Up @@ -336,7 +336,7 @@ mod test_parse_output {

let result = parse_output(&tokens);
assert_eq!(result.unwrap_err().to_string(),
"Parsing error: Expected an identifier, found 1");
"liquid: Expected identifier, found `1`\n");
}

#[test]
Expand All @@ -345,7 +345,7 @@ mod test_parse_output {

let result = parse_output(&tokens);
assert_eq!(result.unwrap_err().to_string(),
"Parsing error: Expected a comma or a pipe, found blabla");
"liquid: Expected `,` | `|`, found `blabla`\n");
}

#[test]
Expand All @@ -354,7 +354,7 @@ mod test_parse_output {

let result = parse_output(&tokens);
assert_eq!(result.unwrap_err().to_string(),
"Parsing error: Expected :, found 1");
"liquid: Expected `:`, found `1`\n");
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/interpreter/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub fn unexpected_value_error<S: ToString>(expected: &str, actual: Option<S>) ->

pub fn unexpected_value_error_string(expected: &str, actual: Option<String>) -> Error {
let actual = actual.unwrap_or_else(|| "nothing".to_owned());
Error::with_msg(format!("Expected {}, found {}", expected, actual))
Error::with_msg(format!("Expected {}, found `{}`", expected, actual))
}

#[derive(Clone, Debug)]
Expand Down
29 changes: 27 additions & 2 deletions src/interpreter/output.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use error::{Error, Result, ResultLiquidChainExt};
use std::fmt;

use itertools;

use error::{Error, Result, ResultLiquidChainExt, ResultLiquidExt};
use value::Value;

use super::Context;
Expand All @@ -20,12 +24,30 @@ impl FilterPrototype {
}
}

impl fmt::Display for FilterPrototype {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f,
"{}: {}",
self.name,
itertools::join(&self.arguments, ", "))
}
}

#[derive(Clone, Debug, PartialEq)]
pub struct Output {
entry: Argument,
filters: Vec<FilterPrototype>,
}

impl fmt::Display for Output {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f,
"{} | {}",
self.entry,
itertools::join(&self.filters, " | "))
}
}

impl Renderable for Output {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
let entry = self.apply_filters(context)?;
Expand Down Expand Up @@ -60,7 +82,10 @@ impl Output {
.map(|a| a.evaluate(context))
.collect();
let arguments = arguments?;
entry = f.filter(&entry, &*arguments).chain("Filter error")?;
entry = f.filter(&entry, &*arguments)
.chain("Filter error")
.context("input", &entry)
.context_with(|| ("args".into(), itertools::join(&arguments, ", ")))?;
}

Ok(entry)
Expand Down
11 changes: 10 additions & 1 deletion src/tags/assign_tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ use interpreter::Renderable;
use compiler::LiquidOptions;
use compiler::Token;
use compiler::{parse_output, expect, unexpected_token_error};
use compiler::ResultLiquidExt;

#[derive(Clone, Debug)]
struct Assign {
dst: String,
src: Output,
}

impl Assign {
fn trace(&self) -> String {
format!("{{% assign {} = {}%}}", self.dst, self.src)
}
}

impl Renderable for Assign {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
let value = self.src.apply_filters(context)?;
let value = self.src
.apply_filters(context)
.trace_with(|| self.trace().into())?;
context.set_global_val(&self.dst, value);
Ok(None)
}
Expand Down
15 changes: 10 additions & 5 deletions src/tags/capture_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ struct Capture {
template: Template,
}

impl Capture {
fn trace(&self) -> String {
format!("{{% capture {} %}}", self.id)
}
}

impl Renderable for Capture {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
let output = match self.template.render(context) {
Ok(Some(s)) => s.clone(),
Ok(None) => "".to_owned(),
Err(x) => return Err(x),
};
let output = self.template
.render(context)
.trace_with(|| self.trace().into())?
.unwrap_or_else(|| "".to_owned());

context.set_global_val(&self.id, Value::scalar(output));
Ok(None)
Expand Down
21 changes: 19 additions & 2 deletions src/tags/case_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ impl CaseOption {
}
Ok(false)
}

fn trace(&self) -> String {
format!("{{% when {} %}}", itertools::join(self.args.iter(), " or "))
}
}

#[derive(Debug)]
Expand All @@ -41,17 +45,30 @@ struct Case {
else_block: Option<Template>,
}

impl Case {
fn trace(&self) -> String {
format!("{{% case {} %}}", self.target)
}
}

impl Renderable for Case {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
let value = self.target.evaluate(context)?;
for case in &self.cases {
if case.evaluate(&value, context)? {
return case.template.render(context);
return case.template
.render(context)
.trace_with(|| case.trace().into())
.trace_with(|| self.trace().into())
.context_with(|| (self.target.to_string().into(), value.to_string()));
}
}

if let Some(ref t) = self.else_block {
return t.render(context);
return t.render(context)
.trace_with(|| "{{% else %}}".to_owned().into())
.trace_with(|| self.trace().into())
.context_with(|| (self.target.to_string().into(), value.to_string()));
}

Ok(None)
Expand Down
13 changes: 11 additions & 2 deletions src/tags/cycle_tag.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use itertools;

use error::Result;
use error::{Result, ResultLiquidExt};

use interpreter::Argument;
use interpreter::Context;
Expand All @@ -15,9 +15,18 @@ struct Cycle {
values: Vec<Argument>,
}

impl Cycle {
fn trace(&self) -> String {
format!("{{% cycle {} %}}",
itertools::join(self.values.iter(), ", "))
}
}

impl Renderable for Cycle {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
let value = context.cycle_element(&self.name, &self.values)?;
let value = context
.cycle_element(&self.name, &self.values)
.trace_with(|| self.trace().into())?;
Ok(value.map(|v| v.to_string()))
}
}
Expand Down
107 changes: 72 additions & 35 deletions src/tags/for_block.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt;
use std::slice::Iter;

use itertools;
Expand All @@ -20,6 +21,31 @@ enum Range {
Counted(Argument, Argument),
}

impl Range {
pub fn evaluate(&self, context: &Context) -> Result<Vec<Value>> {
let range = match *self {
Range::Array(ref array_id) => get_array(context, array_id)?,

Range::Counted(ref start_arg, ref stop_arg) => {
let start = int_argument(start_arg, context, "start")?;
let stop = int_argument(stop_arg, context, "end")?;
(start..stop).map(|x| Value::scalar(x as i32)).collect()
}
};

Ok(range)
}
}

impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Range::Array(ref arr) => write!(f, "{}", arr),
Range::Counted(ref start, ref end) => write!(f, "({}..{})", start, end),
}
}
}

#[derive(Debug)]
struct For {
var_name: String,
Expand All @@ -31,6 +57,16 @@ struct For {
reversed: bool,
}

impl For {
fn trace(&self) -> String {
trace_for_tag(&self.var_name,
&self.range,
self.limit,
self.offset,
self.reversed)
}
}

fn get_array(context: &Context, array_id: &Argument) -> Result<Vec<Value>> {
let array = array_id.evaluate(context)?;
match array {
Expand All @@ -39,49 +75,50 @@ fn get_array(context: &Context, array_id: &Argument) -> Result<Vec<Value>> {
}
}

fn int_argument(arg: &Argument, context: &Context) -> Result<isize> {
fn int_argument(arg: &Argument, context: &Context, arg_name: &str) -> Result<isize> {
let value = arg.evaluate(context)?;

let value =
value
.as_scalar()
.and_then(Scalar::to_integer)
.ok_or_else(|| unexpected_value_error("whole number", Some(value.type_name())))?;
let value = value
.as_scalar()
.and_then(Scalar::to_integer)
.ok_or_else(|| unexpected_value_error("whole number", Some(value.type_name())))
.context_with(|| (arg_name.to_string().into(), value.to_string()))?;

Ok(value as isize)
}

impl Renderable for For {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
let mut range = match self.range {
Range::Array(ref array_id) => get_array(context, array_id)?,
fn for_slice(range: &mut [Value], limit: Option<usize>, offset: usize, reversed: bool) -> &[Value] {
let end = match limit {
Some(n) => offset + n,
None => range.len(),
};

Range::Counted(ref start_arg, ref stop_arg) => {
let start = int_argument(start_arg, context)?;
let stop = int_argument(stop_arg, context)?;
(start..stop).map(|x| Value::scalar(x as i32)).collect()
}
};
let slice = if end > range.len() {
&mut range[offset..]
} else {
&mut range[offset..end]
};

let end = match self.limit {
Some(n) => self.offset + n,
None => range.len(),
};
if reversed {
slice.reverse();
};

let slice = if end > range.len() {
&mut range[self.offset..]
} else {
&mut range[self.offset..end]
};
slice
}

if self.reversed {
slice.reverse();
};
impl Renderable for For {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
let mut range = self.range
.evaluate(context)
.trace_with(|| self.trace().into())?;
let range = for_slice(&mut range, self.limit, self.offset, self.reversed);

match slice.len() {
match range.len() {
0 => {
if let Some(ref t) = self.else_template {
t.render(context)
.trace_with(|| "{{% else %}}".to_owned().into())
.trace_with(|| self.trace().into())
} else {
Ok(None)
}
Expand All @@ -93,7 +130,7 @@ impl Renderable for For {
let mut helper_vars = Object::new();
helper_vars.insert("length".to_owned(), Value::scalar(range_len as i32));

for (i, v) in slice.iter().enumerate() {
for (i, v) in range.iter().enumerate() {
helper_vars.insert("index0".to_owned(), Value::scalar(i as i32));
helper_vars.insert("index".to_owned(), Value::scalar((i + 1) as i32));
helper_vars.insert("rindex0".to_owned(),
Expand All @@ -105,7 +142,11 @@ impl Renderable for For {

scope.set_val("forloop", Value::Object(helper_vars.clone()));
scope.set_val(&self.var_name, v.clone());
let inner = try!(self.item_template.render(&mut scope))
let inner = self.item_template
.render(&mut scope)
.trace_with(|| self.trace().into())
.context_with(|| (self.var_name.clone().into(), v.to_string()))
.context("index", &(i + 1))?
.unwrap_or_else(String::new);
ret = ret + &inner;

Expand Down Expand Up @@ -159,10 +200,6 @@ fn trace_for_tag(var_name: &str,
if reversed {
parameters.push("reversed".to_owned());
}
let range = match *range {
Range::Array(ref arr) => arr.to_string(),
Range::Counted(ref start, ref end) => format!("({}..{})", start, end),
};
format!("{{% for {} in {} {} %}}",
var_name,
range,
Expand Down
Loading

0 comments on commit 73e26cf

Please sign in to comment.