From c8cd40fc1a4958cb10081bc5192ab6b05fb50df1 Mon Sep 17 00:00:00 2001 From: "Romanov N.A." Date: Wed, 12 Aug 2026 13:24:23 +0300 Subject: [PATCH 1/2] JEXL-411: Allow optional leading zeroes in floating point literals JEXL could not parse a floating point literal with an omitted leading zero (e.g. ".1"), so "(1+0.1)*2" evaluated to 2.2 while "(1+.1)*2" raised a parsing error, although ".1" is valid in Java. A leading-dot number is tokenized as "DOT DOT_IDENTIFIER", which is identical to the postfix index access "x.3". The literal is therefore recognized in the parser, in operand position, rather than at the lexer level. FloatLiteral() now also accepts "." (only all-digit DOT_IDENTIFIER images, checked via isAllDigits) and rebuilds the value as "." + image before handing it to NumberParser. Dot-based index/property access is left untouched. Add an arithmetic test covering the JEXL-411 reproduction, leading-dot literals, unary sign and the index-access regression. --- .../apache/commons/jexl3/parser/Parser.jjt | 24 ++++++++++++++++ .../apache/commons/jexl3/ArithmeticTest.java | 28 +++++++++++++++++++ 2 files changed, 52 insertions(+) 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..4ec329fdc 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -107,6 +107,27 @@ public final class Parser extends JexlParser jjtree.reset(); } } + + /** + * 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 + */ + private static boolean isAllDigits(final String image) { + if (image.isEmpty()) { + return false; + } + for (int i = 0; i < image.length(); i++) { + final char c = image.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } } PARSER_END(Parser) @@ -1064,6 +1085,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..a2093e602 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -2341,4 +2341,32 @@ 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)); + // 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("one", jexl.createExpression("array.1").evaluate(jc)); + } } From fecd3ad7325748fdd19aa3411d051e87b830e685 Mon Sep 17 00:00:00 2001 From: Henrib Date: Wed, 12 Aug 2026 14:30:17 +0200 Subject: [PATCH 2/2] JEXL-411: move isAllDigits to JexlParser; - update changes.xml; --- src/changes/changes.xml | 3 +++ .../commons/jexl3/parser/JexlParser.java | 23 +++++++++++++++++++ .../apache/commons/jexl3/parser/Parser.jjt | 21 ----------------- .../apache/commons/jexl3/ArithmeticTest.java | 2 ++ 4 files changed, 28 insertions(+), 21 deletions(-) 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 4ec329fdc..7105234ec 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -107,27 +107,6 @@ public final class Parser extends JexlParser jjtree.reset(); } } - - /** - * 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 - */ - private static boolean isAllDigits(final String image) { - if (image.isEmpty()) { - return false; - } - for (int i = 0; i < image.length(); i++) { - final char c = image.charAt(i); - if (c < '0' || c > '9') { - return false; - } - } - return true; - } } PARSER_END(Parser) diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java index a2093e602..2893f0b3e 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -2362,11 +2362,13 @@ void setOptions(final JexlOptions options) { 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)); } }