A patch from Charlie to provide line numbers when reporting variable related

errors during execution.
This commit is contained in:
Michael Bayne
2010-10-23 16:35:38 +00:00
parent 4f5b3d772d
commit 7ec57412fa
3 changed files with 55 additions and 41 deletions
@@ -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));