diff options
-rw-r--r-- | value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java | 63 | ||||
-rw-r--r-- | value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java | 13 |
2 files changed, 70 insertions, 6 deletions
diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java index 53915161..73e360b4 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java @@ -74,12 +74,20 @@ class Parser { /** * The invariant of this parser is that {@code c} is always the next character of interest. - * This means that we never have to "unget" a character by reading too far. For example, after - * we parse an integer, {@code c} will be the first character after the integer, which is exactly - * the state we will be in when there are no more digits. + * This means that we almost never have to "unget" a character by reading too far. For example, + * after we parse an integer, {@code c} will be the first character after the integer, which is + * exactly the state we will be in when there are no more digits. + * + * <p>Sometimes we need to read two characters ahead, and in that case we use {@link #pushback}. */ private int c; + /** + * A single character of pushback. If this is not negative, the {@link #next()} method will + * return it instead of reading a character. + */ + private int pushback = -1; + Parser(Reader reader, String resourceName, Template.ResourceOpener resourceOpener) throws IOException { this.reader = new LineNumberReader(reader); @@ -148,11 +156,29 @@ class Parser { */ private void next() throws IOException { if (c != EOF) { - c = reader.read(); + if (pushback < 0) { + c = reader.read(); + } else { + c = pushback; + pushback = -1; + } } } /** + * Saves the current character {@code c} to be read again, and sets {@code c} to the given + * {@code c1}. Suppose the text contains {@code xy} and we have just read {@code y}. + * So {@code c == 'y'}. Now if we execute {@code pushback('x')}, we will have + * {@code c == 'x'} and the next call to {@link #next()} will set {@code c == 'y'}. Subsequent + * calls to {@code next()} will continue reading from {@link #reader}. So the pushback + * essentially puts us back in the state we were in before we read {@code y}. + */ + private void pushback(int c1) { + pushback = c; + c = c1; + } + + /** * If {@code c} is a space character, keeps reading until {@code c} is a non-space character or * there are no more characters. */ @@ -562,7 +588,27 @@ class Parser { * * <p>On entry to this method, {@link #c} is the character immediately after the {@code $}. */ - private ReferenceNode parseReference() throws IOException { + private Node parseReference() throws IOException { + if (c == '{') { + next(); + if (!isAsciiLetter(c)) { + return parsePlainText(new StringBuilder("${")); + } + ReferenceNode node = parseReferenceNoBrace(); + expect('}'); + return node; + } else { + return parseReferenceNoBrace(); + } + } + + /** + * Same as {@link #parseReference()}, except it really must be a reference. A {@code $} in + * normal text doesn't start a reference if it is not followed by an identifier. But in an + * expression, for example in {@code #if ($x == 23)}, {@code $} must be followed by an + * identifier. + */ + private ReferenceNode parseRequiredReference() throws IOException { if (c == '{') { next(); ReferenceNode node = parseReferenceNoBrace(); @@ -622,6 +668,11 @@ class Parser { private ReferenceNode parseReferenceMember(ReferenceNode lhs) throws IOException { assert c == '.'; next(); + if (!isAsciiLetter(c)) { + // We've seen something like `$foo.!`, so it turns out it's not a member after all. + pushback('.'); + return lhs; + } String id = parseId("Member"); ReferenceNode reference; if (c == '(') { @@ -868,7 +919,7 @@ class Parser { ExpressionNode node; if (c == '$') { next(); - node = parseReference(); + node = parseRequiredReference(); } else if (c == '"') { node = parseStringLiteral(); } else if (c == '-') { diff --git a/value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java b/value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java index 16b06b83..32bd0103 100644 --- a/value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java +++ b/value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java @@ -203,6 +203,11 @@ public class TemplateTest { compare("=${t.name}=", ImmutableMap.of("t", Thread.currentThread())); } + @Test + public void substituteNotPropertyId() { + compare("$foo.!", ImmutableMap.of("foo", false)); + } + /* TODO(emcmanus): make this work. @Test public void substituteNotPropertyId() { @@ -699,6 +704,14 @@ public class TemplateTest { } @Test + public void badBraceReference() throws IOException { + String template = "line 1\nline 2\nbar${foo.!}baz"; + thrown.expect(ParseException.class); + thrown.expectMessage("Expected }, on line 3, at text starting: .!}baz"); + Template.parseFrom(new StringReader(template)); + } + + @Test public void undefinedMacro() throws IOException { String template = "#oops()"; thrown.expect(ParseException.class); |