diff --git a/src/changes/changes.xml b/src/changes/changes.xml index f79a5b790..904785caf 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -29,7 +29,10 @@ + IntelliJ and VSCode editors (TextMate bundle) support for JEXL. + IllegalStateException parsing a template with string interpolation. + Leading zeroes in floating point numbers should be optional. Pick up commons.jacoco.version from the parent POM. Add messages when throwing NullPointerException. diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java index d79ccca62..b01ca0ba7 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java @@ -932,6 +932,29 @@ protected boolean isAmbiguousStatement(final int semicolon) { return !getFeatures().supportsAmbiguousStatement(); } + + /** + * Checks whether a token image is composed only of decimal digits. + *

Used to recognize a floating point literal with an omitted leading + * zero (e.g. {@code .1}) that is tokenized as {@code DOT DOT_IDENTIFIER}.

+ * + * @param image the token image + * @return true if every character is a decimal digit + */ + protected boolean isAllDigits(final String image) { + int len = image != null ? image.length() : 0; + if (len == 0) { + return false; + } + for (int i = 0; i < len; ++i) { + final char c = image.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + /** * Called by parser at end of node construction. *

diff --git a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt index 2d9eb1a20..7105234ec 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -1064,6 +1064,9 @@ void FloatLiteral() #NumberLiteral: { t= { jjtThis.setReal(t.image); } + | + t= + { if (!isAllDigits(t.image)) { throwParsingException(t); } jjtThis.setReal("." + t.image); } } void StringLiteral() : diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java index 09433b28b..2893f0b3e 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -2341,4 +2341,34 @@ void setOptions(final JexlOptions options) { this.options = options; } } + + @Test void testLeadingDotFloatLiteral() { + final JexlEngine jexl = new JexlBuilder().create(); + final JexlContext jc = new MapContext(); + // the exact case from JEXL-411 + assertEquals(2.2d, jexl.createExpression("(1+.1)*2").evaluate(jc)); + // the regression case that already worked + assertEquals(2.2d, jexl.createExpression("(1+0.1)*2").evaluate(jc)); + // standalone and compound leading-dot literals + assertEquals(0.1d, jexl.createExpression(".1").evaluate(jc)); + assertEquals(1.0d, jexl.createExpression(".5 + .5").evaluate(jc)); + assertEquals(0.55d, jexl.createExpression(".55").evaluate(jc)); + assertEquals(0.1d, jexl.createExpression("-.1 + .2").evaluate(jc)); + assertEquals(-0.1d, jexl.createExpression(".1 + -.2").evaluate(jc)); + assertEquals(0.1d, jexl.createExpression("-.1+.2").evaluate(jc)); + assertEquals(-0.1d, jexl.createExpression(".1+-.2").evaluate(jc)); + assertEquals(1.4d, jexl.createExpression(".5 - -.9").evaluate(jc)); + assertEquals(0.4d, jexl.createExpression("-.5 - -.9").evaluate(jc)); + assertEquals(-1.4d, jexl.createExpression("-.5+-.9").evaluate(jc)); + // unary handling + assertEquals(-0.1d, jexl.createExpression("-.1").evaluate(jc)); + assertEquals(0.1d, jexl.createExpression("+.1").evaluate(jc)); + // property / index access via a dot must keep working (not parsed as a float) + final List array = new java.util.ArrayList<>(); + array.add("zero"); + array.add("one"); + jc.set("array", array); + assertEquals("zero", jexl.createExpression("array.0").evaluate(jc)); + assertEquals("one", jexl.createExpression("array.1").evaluate(jc)); + } }