A patch from Charlie to provide line numbers when reporting variable related
errors during execution.
This commit is contained in:
@@ -11,7 +11,6 @@ import java.lang.reflect.Array;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides <a href="http://mustache.github.com/">Mustache</a> templating services.
|
* Provides <a href="http://mustache.github.com/">Mustache</a> templating services.
|
||||||
@@ -72,9 +71,9 @@ public class Mustache
|
|||||||
// a hand-rolled parser; whee!
|
// a hand-rolled parser; whee!
|
||||||
Accumulator accum = new Accumulator(compiler);
|
Accumulator accum = new Accumulator(compiler);
|
||||||
char start1 = '{', start2 = '{', end1 = '}', end2 = '}';
|
char start1 = '{', start2 = '{', end1 = '}', end2 = '}';
|
||||||
int state = TEXT, startPos = 0, endPos = 0;
|
int state = TEXT;
|
||||||
StringBuilder text = new StringBuilder();
|
StringBuilder text = new StringBuilder();
|
||||||
int line = 0;
|
int line = 1;
|
||||||
boolean skipNewline = false;
|
boolean skipNewline = false;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -205,8 +204,8 @@ public class Mustache
|
|||||||
|
|
||||||
protected static String escapeHTML (String text)
|
protected static String escapeHTML (String text)
|
||||||
{
|
{
|
||||||
for (int ii = 0; ii < ATTR_ESCAPES.length; ii++) {
|
for (String[] escape : ATTR_ESCAPES) {
|
||||||
text = text.replace(ATTR_ESCAPES[ii][0], ATTR_ESCAPES[ii][1]);
|
text = text.replace(escape[0], escape[1]);
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
@@ -234,7 +233,7 @@ public class Mustache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Accumulator addTagSegment (StringBuilder accum, final int line) {
|
public Accumulator addTagSegment (StringBuilder accum, final int tagLine) {
|
||||||
final Accumulator outer = this;
|
final Accumulator outer = this;
|
||||||
String tag = accum.toString().trim();
|
String tag = accum.toString().trim();
|
||||||
final String tag1 = tag.substring(1).trim();
|
final String tag1 = tag.substring(1).trim();
|
||||||
@@ -242,7 +241,7 @@ public class Mustache
|
|||||||
|
|
||||||
switch (tag.charAt(0)) {
|
switch (tag.charAt(0)) {
|
||||||
case '#':
|
case '#':
|
||||||
requireNoNewlines(tag, line);
|
requireNoNewlines(tag, tagLine);
|
||||||
return new Accumulator(_compiler) {
|
return new Accumulator(_compiler) {
|
||||||
@Override public boolean skipNewline () {
|
@Override public boolean skipNewline () {
|
||||||
// if we just opened this section, we want to skip a newline
|
// if we just opened this section, we want to skip a newline
|
||||||
@@ -250,17 +249,17 @@ public class Mustache
|
|||||||
}
|
}
|
||||||
@Override public Template.Segment[] finish () {
|
@Override public Template.Segment[] finish () {
|
||||||
throw new MustacheException("Section missing close tag " +
|
throw new MustacheException("Section missing close tag " +
|
||||||
"[line=" + line + ", tag=" + tag1 + "]");
|
"[line=" + tagLine + ", tag=" + tag1 + "]");
|
||||||
}
|
}
|
||||||
@Override protected Accumulator addCloseSectionSegment (String itag, int line) {
|
@Override protected Accumulator addCloseSectionSegment (String itag, int line) {
|
||||||
requireSameName(tag1, itag, line);
|
requireSameName(tag1, itag, line);
|
||||||
outer._segs.add(new SectionSegment(itag, super.finish()));
|
outer._segs.add(new SectionSegment(itag, super.finish(), tagLine));
|
||||||
return outer;
|
return outer;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
case '^':
|
case '^':
|
||||||
requireNoNewlines(tag, line);
|
requireNoNewlines(tag, tagLine);
|
||||||
return new Accumulator(_compiler) {
|
return new Accumulator(_compiler) {
|
||||||
@Override public boolean skipNewline () {
|
@Override public boolean skipNewline () {
|
||||||
// if we just opened this section, we want to skip a newline
|
// if we just opened this section, we want to skip a newline
|
||||||
@@ -268,31 +267,31 @@ public class Mustache
|
|||||||
}
|
}
|
||||||
@Override public Template.Segment[] finish () {
|
@Override public Template.Segment[] finish () {
|
||||||
throw new MustacheException("Inverted section missing close tag " +
|
throw new MustacheException("Inverted section missing close tag " +
|
||||||
"[line=" + line + ", tag=" + tag1 + "]");
|
"[line=" + tagLine + ", tag=" + tag1 + "]");
|
||||||
}
|
}
|
||||||
@Override protected Accumulator addCloseSectionSegment (String itag, int line) {
|
@Override protected Accumulator addCloseSectionSegment (String itag, int line) {
|
||||||
requireSameName(tag1, itag, line);
|
requireSameName(tag1, itag, line);
|
||||||
outer._segs.add(new InvertedSectionSegment(itag, super.finish()));
|
outer._segs.add(new InvertedSectionSegment(itag, super.finish(), tagLine));
|
||||||
return outer;
|
return outer;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
case '/':
|
case '/':
|
||||||
requireNoNewlines(tag, line);
|
requireNoNewlines(tag, tagLine);
|
||||||
return addCloseSectionSegment(tag1, line);
|
return addCloseSectionSegment(tag1, tagLine);
|
||||||
|
|
||||||
case '!':
|
case '!':
|
||||||
// comment!, ignore
|
// comment!, ignore
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
case '&':
|
case '&':
|
||||||
requireNoNewlines(tag, line);
|
requireNoNewlines(tag, tagLine);
|
||||||
_segs.add(new VariableSegment(tag1, false));
|
_segs.add(new VariableSegment(tag1, false, tagLine));
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
requireNoNewlines(tag, line);
|
requireNoNewlines(tag, tagLine);
|
||||||
_segs.add(new VariableSegment(tag, _compiler.escapeHTML));
|
_segs.add(new VariableSegment(tag, _compiler.escapeHTML, tagLine));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,7 +307,7 @@ public class Mustache
|
|||||||
|
|
||||||
protected static void requireNoNewlines (String tag, int line) {
|
protected static void requireNoNewlines (String tag, int line) {
|
||||||
if (tag.indexOf("\n") != -1 || tag.indexOf("\r") != -1) {
|
if (tag.indexOf("\n") != -1 || tag.indexOf("\r") != -1) {
|
||||||
throw new MustacheException("Invalid tag name: contains newlne " +
|
throw new MustacheException("Invalid tag name: contains newline " +
|
||||||
"[line=" + line + ", tag=" + tag + "]");
|
"[line=" + line + ", tag=" + tag + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,20 +338,22 @@ public class Mustache
|
|||||||
|
|
||||||
/** A helper class for named segments. */
|
/** A helper class for named segments. */
|
||||||
protected static abstract class NamedSegment extends Template.Segment {
|
protected static abstract class NamedSegment extends Template.Segment {
|
||||||
protected NamedSegment (String name) {
|
protected NamedSegment (String name, int line) {
|
||||||
_name = name.intern();
|
_name = name.intern();
|
||||||
|
_line = line;
|
||||||
}
|
}
|
||||||
protected final String _name;
|
protected final String _name;
|
||||||
|
protected final int _line;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A segment that substitutes the contents of a variable. */
|
/** A segment that substitutes the contents of a variable. */
|
||||||
protected static class VariableSegment extends NamedSegment {
|
protected static class VariableSegment extends NamedSegment {
|
||||||
public VariableSegment (String name, boolean escapeHTML) {
|
public VariableSegment (String name, boolean escapeHTML, int line) {
|
||||||
super(name);
|
super(name, line);
|
||||||
_escapeHTML = escapeHTML;
|
_escapeHTML = escapeHTML;
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
Object value = tmpl.getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name, _line);
|
||||||
// TODO: configurable behavior on missing values
|
// TODO: configurable behavior on missing values
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
String text = String.valueOf(value);
|
String text = String.valueOf(value);
|
||||||
@@ -364,8 +365,8 @@ public class Mustache
|
|||||||
|
|
||||||
/** A helper class for compound segments. */
|
/** A helper class for compound segments. */
|
||||||
protected static abstract class CompoundSegment extends NamedSegment {
|
protected static abstract class CompoundSegment extends NamedSegment {
|
||||||
protected CompoundSegment (String name, Template.Segment[] segs) {
|
protected CompoundSegment (String name, Template.Segment[] segs, int line) {
|
||||||
super(name);
|
super(name, line);
|
||||||
_segs = segs;
|
_segs = segs;
|
||||||
}
|
}
|
||||||
protected void executeSegs (Template tmpl, Template.Context ctx, Writer out) {
|
protected void executeSegs (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
@@ -378,11 +379,11 @@ public class Mustache
|
|||||||
|
|
||||||
/** A segment that represents a section. */
|
/** A segment that represents a section. */
|
||||||
protected static class SectionSegment extends CompoundSegment {
|
protected static class SectionSegment extends CompoundSegment {
|
||||||
public SectionSegment (String name, Template.Segment[] segs) {
|
public SectionSegment (String name, Template.Segment[] segs, int line) {
|
||||||
super(name, segs);
|
super(name, segs, line);
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
Object value = tmpl.getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name, _line);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return; // TODO: configurable behavior on missing values
|
return; // TODO: configurable behavior on missing values
|
||||||
}
|
}
|
||||||
@@ -416,11 +417,11 @@ public class Mustache
|
|||||||
|
|
||||||
/** A segment that represents an inverted section. */
|
/** A segment that represents an inverted section. */
|
||||||
protected static class InvertedSectionSegment extends CompoundSegment {
|
protected static class InvertedSectionSegment extends CompoundSegment {
|
||||||
public InvertedSectionSegment (String name, Template.Segment[] segs) {
|
public InvertedSectionSegment (String name, Template.Segment[] segs, int line) {
|
||||||
super(name, segs);
|
super(name, segs, line);
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
Object value = tmpl.getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name, _line);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values
|
executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public class Template
|
|||||||
* @param ctx the context in which to look up the variable.
|
* @param ctx the context in which to look up the variable.
|
||||||
* @param name the name of the variable to be resolved, which must be an interned string.
|
* @param name the name of the variable to be resolved, which must be an interned string.
|
||||||
*/
|
*/
|
||||||
protected Object getValue (Context ctx, String name)
|
protected Object getValue (Context ctx, String name, int line)
|
||||||
{
|
{
|
||||||
// if we're dealing with a compound key, resolve each component and use the result to
|
// if we're dealing with a compound key, resolve each component and use the result to
|
||||||
// resolve the subsequent component and so forth
|
// resolve the subsequent component and so forth
|
||||||
@@ -80,17 +80,17 @@ public class Template
|
|||||||
// we want to allow the first component of a compound key to be located in a parent
|
// we want to allow the first component of a compound key to be located in a parent
|
||||||
// context, but once we're selecting sub-components, they must only be resolved in the
|
// context, but once we're selecting sub-components, they must only be resolved in the
|
||||||
// object that represents that component
|
// object that represents that component
|
||||||
Object data = getValue(ctx, comps[0].intern());
|
Object data = getValue(ctx, comps[0].intern(), line);
|
||||||
for (int ii = 1; ii < comps.length; ii++) {
|
for (int ii = 1; ii < comps.length; ii++) {
|
||||||
// generate more helpful error message
|
// generate more helpful error message
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
throw new NullPointerException(
|
throw new NullPointerException(
|
||||||
"Null context for compound variable '" + name + "'. " +
|
"Null context for compound variable '" + name + "' on line " + line +
|
||||||
"The component previous to '" + comps[ii] + "' resolved to null.");
|
". '" + comps[ii - 1] + "' resolved to null.");
|
||||||
}
|
}
|
||||||
// once we step into a composite key, we drop the ability to query our parent
|
// once we step into a composite key, we drop the ability to query our parent
|
||||||
// contexts; that would be weird and confusing
|
// contexts; that would be weird and confusing
|
||||||
data = getValueIn(data, comps[ii].intern());
|
data = getValueIn(data, comps[ii].intern(), line);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -105,20 +105,22 @@ public class Template
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (ctx != null) {
|
while (ctx != null) {
|
||||||
Object value = getValueIn(ctx.data, name);
|
Object value = getValueIn(ctx.data, name, line);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
ctx = ctx.parent;
|
ctx = ctx.parent;
|
||||||
}
|
}
|
||||||
// we've popped all the way off the top of our stack of contexts, so fail
|
// we've popped all the way off the top of our stack of contexts, so fail
|
||||||
throw new MustacheException("No key, method or field with name '" + name + "'");
|
throw new MustacheException(
|
||||||
|
"No key, method or field with name '" + name + "' on line " + line);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Object getValueIn (Object data, String name)
|
protected Object getValueIn (Object data, String name, int line)
|
||||||
{
|
{
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
throw new NullPointerException("Null context for variable '" + name + "'");
|
throw new NullPointerException(
|
||||||
|
"Null context for variable '" + name + "' on line " + line);
|
||||||
}
|
}
|
||||||
|
|
||||||
Key key = new Key(data.getClass(), name);
|
Key key = new Key(data.getClass(), name);
|
||||||
@@ -145,7 +147,8 @@ public class Template
|
|||||||
_fcache.put(key, fetcher);
|
_fcache.put(key, fetcher);
|
||||||
return value;
|
return value;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new MustacheException("Failure fetching variable '" + name + "'", e);
|
throw new MustacheException(
|
||||||
|
"Failure fetching variable '" + name + "' on line " + line, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,16 @@ public class MustacheTest
|
|||||||
context("things", Arrays.asList("foo", "bar", "baz")));
|
context("things", Arrays.asList("foo", "bar", "baz")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void testLineReporting () {
|
||||||
|
String tmpl = "first line\n{{nonexistent}}\nsecond line";
|
||||||
|
try {
|
||||||
|
Mustache.compiler().compile(tmpl).execute(new Object());
|
||||||
|
fail("Referencing a nonexistent variable should throw MustacheException");
|
||||||
|
} catch (MustacheException e) {
|
||||||
|
assertTrue(e.getMessage().contains("line 2"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void test (String expected, String template, Object ctx)
|
protected void test (String expected, String template, Object ctx)
|
||||||
{
|
{
|
||||||
assertEquals(expected, Mustache.compiler().compile(template).execute(ctx));
|
assertEquals(expected, Mustache.compiler().compile(template).execute(ctx));
|
||||||
|
|||||||
Reference in New Issue
Block a user