Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions src/main/java/com/googlecode/aviator/lexer/ExpressionLexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -434,22 +434,70 @@ 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);
return this.symbolTable.reserve(variable);
}


/**
* Try to absorb a mid-chain integer-literal index into the variable lexeme, e.g. the "[0]" in
* "foo.bars[0].name".
*
* <p>
* 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.
*
* <p>
* 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Comment thread
killme2008 marked this conversation as resolved.


@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 ");
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -149,4 +150,35 @@ public void testPropertyChainWhenNoFullKey() {
public void testInvalidPropertyChainWithEmptySegment() {
AviatorEvaluator.execute("a..b");
}


@Test
public void testDottedVarWithDynamicIndex() {
Map<String, Object> env = new HashMap<String, Object>();
Map<String, Object> a = new HashMap<String, Object>();
a.put("list", Arrays.asList(10, 20, 30));
env.put("a", a);
env.put("i", 1);

// Dynamic index on a dotted path must be evaluated as an expression.
assertEquals(20, AviatorEvaluator.execute("a.list[i]", env));
assertEquals(30, AviatorEvaluator.execute("a.list[i + 1]", env));
// A literal index expression is also evaluated, not parsed as a bare literal key.
assertEquals(30, AviatorEvaluator.execute("a.list[0 + 2]", env));
// Trailing literal index still works.
assertEquals(10, AviatorEvaluator.execute("a.list[0]", env));
}


@Test
public void testDottedVarWithMultiDimensionalIndex() {
Map<String, Object> env = new HashMap<String, Object>();
Map<String, Object> a = new HashMap<String, Object>();
a.put("grid", Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4)));
env.put("a", a);
env.put("i", 1);

assertEquals(2, AviatorEvaluator.execute("a.grid[0][1]", env));
assertEquals(3, AviatorEvaluator.execute("a.grid[i][0]", env));
}
}
Loading