diff --git a/src/main/java/com/googlecode/aviator/lexer/ExpressionLexer.java b/src/main/java/com/googlecode/aviator/lexer/ExpressionLexer.java index e7564c37..392492e9 100644 --- a/src/main/java/com/googlecode/aviator/lexer/ExpressionLexer.java +++ b/src/main/java/com/googlecode/aviator/lexer/ExpressionLexer.java @@ -434,15 +434,23 @@ private Token> scanVariable() { StringBuilder sb = new StringBuilder(); boolean hasDot = false; - do { - if (this.peek == '.') { - hasDot = true; + // The first character is a valid Java identifier start, never a dot. + sb.append(this.peek); + nextChar(); + while (true) { + if (Character.isJavaIdentifierPart(this.peek) || this.peek == '.') { + if (this.peek == '.') { + hasDot = true; + } + sb.append(this.peek); + nextChar(); + } else if (hasDot && this.peek == '[' && tryAbsorbChainedIndex(sb)) { + // The "[digits]" text has been appended and the lexer now points at the + // following '.', which the next iteration consumes. + } else { + break; } - sb.append(this.peek); - nextChar(); - // Only allow [] after a dot has been seen (property access syntax) - } while (Character.isJavaIdentifierPart(this.peek) || this.peek == '.' - || (hasDot && (this.peek == '[' || this.peek == ']'))); + } String lexeme = sb.toString(); Variable variable = new Variable(lexeme, this.lineNo, startIndex); @@ -450,6 +458,46 @@ private Token> scanVariable() { } + /** + * Try to absorb a mid-chain integer-literal index into the variable lexeme, e.g. the "[0]" in + * "foo.bars[0].name". + * + *
+ * Only a non-negative integer literal index immediately followed by another property segment + * ('.') is absorbed, because such a chain cannot be expressed through the parser's array-access + * handling. Dynamic indices (foo.bars[i]), expression indices (foo.bars[i + 1]), trailing indices + * (foo.bars[0]) and multi-dimensional indices (foo.bars[0][1]) are left untouched so the parser + * resolves them via {@code getElement}, which preserves dynamic and multi-dimensional indexing. + * + *
+ * On success the "[digits]" text is appended to {@code sb} and the lexer is positioned at the
+ * following '.'. On failure the lexer is restored to the original '[' and the method returns
+ * false.
+ *
+ * @param sb the variable lexeme being built, positioned at '['
+ * @return true if a chained literal index was absorbed
+ */
+ private boolean tryAbsorbChainedIndex(final StringBuilder sb) {
+ int mark = this.iterator.getIndex(); // points at '['
+ StringBuilder digits = new StringBuilder();
+ nextChar(); // skip '['
+ while (Character.isDigit(this.peek)) {
+ digits.append(this.peek);
+ nextChar();
+ }
+ if (digits.length() > 0 && this.peek == ']') {
+ nextChar(); // skip ']'
+ if (this.peek == '.') {
+ sb.append('[').append(digits).append(']');
+ return true;
+ }
+ }
+ // Not a chained literal index: restore the lexer to the original '['.
+ this.peek = this.iterator.setIndex(mark);
+ return false;
+ }
+
+
/**
* Scan operator character.
*
diff --git a/src/test/java/com/googlecode/aviator/lexer/ExpressionLexerUnitTest.java b/src/test/java/com/googlecode/aviator/lexer/ExpressionLexerUnitTest.java
index 242a1a64..3033ec31 100644
--- a/src/test/java/com/googlecode/aviator/lexer/ExpressionLexerUnitTest.java
+++ b/src/test/java/com/googlecode/aviator/lexer/ExpressionLexerUnitTest.java
@@ -453,6 +453,78 @@ public void testNormalVarWithDotOnly() {
}
+ @Test
+ public void testDottedVarWithDynamicIndexNotAbsorbed() {
+ // A dynamic index must not be swallowed into the variable lexeme; it is left to
+ // the parser's array-access handling: variable "a.b", then '[', 'i', ']'.
+ this.lexer = new ExpressionLexer(this.instance, "a.b[i]");
+ Token> token = this.lexer.scan();
+ assertEquals(TokenType.Variable, token.getType());
+ assertEquals("a.b", token.getValue(null));
+
+ token = this.lexer.scan();
+ assertEquals(TokenType.Char, token.getType());
+ assertEquals('[', token.getValue(null));
+
+ token = this.lexer.scan();
+ assertEquals(TokenType.Variable, token.getType());
+ assertEquals("i", token.getValue(null));
+
+ token = this.lexer.scan();
+ assertEquals(TokenType.Char, token.getType());
+ assertEquals(']', token.getValue(null));
+ assertNull(this.lexer.scan());
+ }
+
+
+ @Test
+ public void testDottedVarWithTrailingLiteralIndexNotAbsorbed() {
+ // A trailing literal index (not followed by another '.') is left to the parser too.
+ this.lexer = new ExpressionLexer(this.instance, "a.b[0]");
+ Token> token = this.lexer.scan();
+ assertEquals(TokenType.Variable, token.getType());
+ assertEquals("a.b", token.getValue(null));
+
+ token = this.lexer.scan();
+ assertEquals(TokenType.Char, token.getType());
+ assertEquals('[', token.getValue(null));
+
+ token = this.lexer.scan();
+ assertEquals(TokenType.Number, token.getType());
+ assertEquals(0, token.getValue(null));
+
+ token = this.lexer.scan();
+ assertEquals(TokenType.Char, token.getType());
+ assertEquals(']', token.getValue(null));
+ assertNull(this.lexer.scan());
+ }
+
+
+ @Test
+ public void testDottedVarWithMultiDimensionalIndexNotAbsorbed() {
+ // Consecutive indices are left to the parser (no mid-chain '.' between them).
+ this.lexer = new ExpressionLexer(this.instance, "a.b[0][1]");
+ Token> token = this.lexer.scan();
+ assertEquals(TokenType.Variable, token.getType());
+ assertEquals("a.b", token.getValue(null));
+
+ token = this.lexer.scan();
+ assertEquals(TokenType.Char, token.getType());
+ assertEquals('[', token.getValue(null));
+ }
+
+
+ @Test
+ public void testDottedVarWithMidChainLiteralIndexAbsorbed() {
+ // A literal index followed by another property segment is part of the lexeme.
+ this.lexer = new ExpressionLexer(this.instance, "a.b[0].c");
+ Token> token = this.lexer.scan();
+ assertEquals(TokenType.Variable, token.getType());
+ assertEquals("a.b[0].c", token.getValue(null));
+ assertNull(this.lexer.scan());
+ }
+
+
@Test
public void testExpression_Logic_Join() {
this.lexer = new ExpressionLexer(this.instance, "a || c ");
diff --git a/src/test/java/com/googlecode/aviator/test/function/QuoteVarTest.java b/src/test/java/com/googlecode/aviator/test/function/QuoteVarTest.java
index 19d95114..ee23ae34 100644
--- a/src/test/java/com/googlecode/aviator/test/function/QuoteVarTest.java
+++ b/src/test/java/com/googlecode/aviator/test/function/QuoteVarTest.java
@@ -1,5 +1,6 @@
package com.googlecode.aviator.test.function;
+import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@@ -149,4 +150,35 @@ public void testPropertyChainWhenNoFullKey() {
public void testInvalidPropertyChainWithEmptySegment() {
AviatorEvaluator.execute("a..b");
}
+
+
+ @Test
+ public void testDottedVarWithDynamicIndex() {
+ Map