From a3705632c133457245f164b2a524bf831a24f31f Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:09:28 +0200 Subject: [PATCH 1/2] Polish SpEL Ternary and Elvis operators --- .../expression/spel/ast/Elvis.java | 8 ++--- .../expression/spel/ast/Ternary.java | 29 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java index e61a8f5074a..f4933a70f3b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java @@ -100,6 +100,7 @@ public class Elvis extends SpelNodeImpl { public void generateCode(MethodVisitor mv, CodeFlow cf) { // exit type descriptor can be null if both components are literal expressions computeExitTypeDescriptor(); + cf.enterCompilationScope(); this.children[0].generateCode(mv, cf); String lastDesc = cf.lastDescriptor(); @@ -131,10 +132,9 @@ public class Elvis extends SpelNodeImpl { } private void computeExitTypeDescriptor() { - if (this.exitTypeDescriptor == null && this.children[0].exitTypeDescriptor != null && - this.children[1].exitTypeDescriptor != null) { - String conditionDescriptor = this.children[0].exitTypeDescriptor; - String ifNullValueDescriptor = this.children[1].exitTypeDescriptor; + String conditionDescriptor = this.children[0].exitTypeDescriptor; + String ifNullValueDescriptor = this.children[1].exitTypeDescriptor; + if (this.exitTypeDescriptor == null && conditionDescriptor != null && ifNullValueDescriptor != null) { if (ObjectUtils.nullSafeEquals(conditionDescriptor, ifNullValueDescriptor)) { this.exitTypeDescriptor = conditionDescriptor; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java index 5d6f6422143..6ea722eab99 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java @@ -66,20 +66,6 @@ public class Ternary extends SpelNodeImpl { return "(" + getChild(0).toStringAST() + " ? " + getChild(1).toStringAST() + " : " + getChild(2).toStringAST() + ")"; } - private void computeExitTypeDescriptor() { - String leftDescriptor = this.children[1].exitTypeDescriptor; - String rightDescriptor = this.children[2].exitTypeDescriptor; - if (this.exitTypeDescriptor == null && leftDescriptor != null && rightDescriptor != null) { - if (ObjectUtils.nullSafeEquals(leftDescriptor, rightDescriptor)) { - this.exitTypeDescriptor = leftDescriptor; - } - else { - // Use the easiest to compute common supertype - this.exitTypeDescriptor = "Ljava/lang/Object"; - } - } - } - @Override public boolean isCompilable() { SpelNodeImpl condition = this.children[0]; @@ -97,6 +83,7 @@ public class Ternary extends SpelNodeImpl { // having been computed, so we must ensure the exit descriptor has been // computed before proceeding. computeExitTypeDescriptor(); + cf.enterCompilationScope(); this.children[0].generateCode(mv, cf); String lastDesc = cf.lastDescriptor(); @@ -130,4 +117,18 @@ public class Ternary extends SpelNodeImpl { cf.pushDescriptor(this.exitTypeDescriptor); } + private void computeExitTypeDescriptor() { + String leftDescriptor = this.children[1].exitTypeDescriptor; + String rightDescriptor = this.children[2].exitTypeDescriptor; + if (this.exitTypeDescriptor == null && leftDescriptor != null && rightDescriptor != null) { + if (ObjectUtils.nullSafeEquals(leftDescriptor, rightDescriptor)) { + this.exitTypeDescriptor = leftDescriptor; + } + else { + // Use the easiest to compute common supertype + this.exitTypeDescriptor = "Ljava/lang/Object"; + } + } + } + } From 28f78f435e768e7ce7d974f82ba7ced700c5848b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:11:07 +0200 Subject: [PATCH 2/2] Introduce missing tests for immediate SpEL compilation for Elvis operator --- .../expression/spel/ast/Elvis.java | 6 +++++- .../spel/SpelCompilationCoverageTests.java | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java index f4933a70f3b..4098850c265 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java @@ -98,7 +98,11 @@ public class Elvis extends SpelNodeImpl { @Override public void generateCode(MethodVisitor mv, CodeFlow cf) { - // exit type descriptor can be null if both components are literal expressions + // If both elements are literals and the expression was not previously + // evaluated in interpreted mode -- or if getValueInternal() did not + // properly invoke computeExitTypeDescriptor() -- we may get here without + // the exit descriptor having been computed, so we must ensure the exit + // descriptor has been computed before proceeding. computeExitTypeDescriptor(); cf.enterCompilationScope(); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 1d0727ec44d..0ecf23aca17 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -1470,6 +1470,23 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); } + @Test + void elvisWithImmediateCompilation() { + expression = parser.parseExpression("'a' ?: 'b'"); + // Both literals, so we can compile immediately without having previously + // evaluated the expression. + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("a"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + + expression = parser.parseExpression("null ?: 'a'"); + // Both literals, so we can compile immediately without having previously + // evaluated the expression. + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("a"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + @Test // gh-19758 void elvisMiscellaneous() { SpelParserConfiguration configuration = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null);