mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Compile SpEL expressions that use Optional with null-safe & Elvis operators
In Spring Framework 7.0, we introduced support for using `Optional` with the null-safe and Elvis operators in SpEL expressions; however, such expressions were previously not compilable. To address that, this commit introduces a new insertOptionalUnwrapIfNecessary() method in CodeFlow which effectively inserts byte code instructions for `myOptional.orElse(null)`, and the Elvis, Indexer, MethodReference, and PropertyOrFieldReference implementations have been modified to track the need to unwrap an `Optional` in compiled mode and delegate to insertOptionalUnwrapIfNecessary() accordingly. See gh-20433 See gh-36331 Closes gh-36330
This commit is contained in:
@@ -531,7 +531,6 @@ following kinds of expressions cannot be compiled.
|
||||
* Expressions relying on the conversion service
|
||||
* Expressions using custom resolvers
|
||||
* Expressions using overloaded operators
|
||||
* Expressions using `Optional` with the null-safe or Elvis operator
|
||||
* Expressions using array construction syntax
|
||||
* Expressions using selection or projection
|
||||
* Expressions using bean references
|
||||
|
||||
@@ -1017,6 +1017,24 @@ public class CodeFlow implements Opcodes {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If the provided descriptor represents a {@link java.util.Optional}, insert
|
||||
* the necessary bytecode to unwrap it.
|
||||
* <p>An empty {@code Optional} will be replaced with {@code null}.
|
||||
* @param mv the method visitor into which instructions should be inserted
|
||||
* @param descriptor the descriptor of a type that may or may not need unwrapping
|
||||
* @since 7.1
|
||||
* @see java.util.Optional#orElse(Object)
|
||||
*/
|
||||
public static void insertOptionalUnwrapIfNecessary(MethodVisitor mv, @Nullable String descriptor) {
|
||||
if ("Ljava/util/Optional".equals(descriptor)) {
|
||||
// Push 'null' onto the stack as the argument for orElse
|
||||
mv.visitInsn(ACONST_NULL);
|
||||
// Invoke java.util.Optional.orElse(null)
|
||||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/util/Optional", "orElse",
|
||||
"(Ljava/lang/Object;)Ljava/lang/Object;", false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to generate fields.
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.springframework.expression.spel.ast;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.asm.Label;
|
||||
import org.springframework.asm.MethodVisitor;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
@@ -43,6 +45,15 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class Elvis extends SpelNodeImpl {
|
||||
|
||||
/**
|
||||
* Tracks the descriptor for the value contained in an {@link Optional} that
|
||||
* was unwrapped in {@link #getValueInternal(ExpressionState)} using the
|
||||
* null-safe operator.
|
||||
* @since 7.1
|
||||
*/
|
||||
private @Nullable String unwrappedOptionalDescriptor;
|
||||
|
||||
|
||||
public Elvis(int startPos, int endPos, SpelNodeImpl... args) {
|
||||
super(startPos, endPos, args);
|
||||
}
|
||||
@@ -65,10 +76,10 @@ public class Elvis extends SpelNodeImpl {
|
||||
Object leftHandValue = leftHandTypedValue.getValue();
|
||||
|
||||
if (leftHandValue instanceof Optional<?> optional) {
|
||||
// Compilation is currently not supported for Optional with the Elvis operator.
|
||||
this.exitTypeDescriptor = null;
|
||||
if (optional.isPresent()) {
|
||||
result = new TypedValue(optional.get());
|
||||
Object value = optional.get();
|
||||
this.unwrappedOptionalDescriptor = CodeFlow.toDescriptor(value.getClass());
|
||||
result = new TypedValue(value);
|
||||
}
|
||||
else {
|
||||
result = this.children[1].getValueInternal(state);
|
||||
@@ -94,7 +105,8 @@ public class Elvis extends SpelNodeImpl {
|
||||
public boolean isCompilable() {
|
||||
SpelNodeImpl condition = this.children[0];
|
||||
SpelNodeImpl ifNullValue = this.children[1];
|
||||
String conditionDescriptor = condition.exitTypeDescriptor;
|
||||
String conditionDescriptor = (this.unwrappedOptionalDescriptor != null ?
|
||||
this.unwrappedOptionalDescriptor : condition.exitTypeDescriptor);
|
||||
String ifNullValueDescriptor = ifNullValue.exitTypeDescriptor;
|
||||
|
||||
return (condition.isCompilable() && ifNullValue.isCompilable() &&
|
||||
@@ -116,7 +128,12 @@ public class Elvis extends SpelNodeImpl {
|
||||
this.children[0].generateCode(mv, cf);
|
||||
String lastDesc = cf.lastDescriptor();
|
||||
Assert.state(lastDesc != null, "No last descriptor");
|
||||
CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0));
|
||||
if ("Ljava/util/Optional".equals(lastDesc)) {
|
||||
CodeFlow.insertOptionalUnwrapIfNecessary(mv, lastDesc);
|
||||
}
|
||||
else {
|
||||
CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0));
|
||||
}
|
||||
cf.exitCompilationScope();
|
||||
|
||||
mv.visitInsn(DUP);
|
||||
@@ -147,7 +164,8 @@ public class Elvis extends SpelNodeImpl {
|
||||
private void computeExitTypeDescriptor() {
|
||||
SpelNodeImpl condition = this.children[0];
|
||||
SpelNodeImpl ifNullValue = this.children[1];
|
||||
String conditionDescriptor = condition.exitTypeDescriptor;
|
||||
String conditionDescriptor = (this.unwrappedOptionalDescriptor != null ?
|
||||
this.unwrappedOptionalDescriptor : condition.exitTypeDescriptor);
|
||||
String ifNullValueDescriptor = ifNullValue.exitTypeDescriptor;
|
||||
|
||||
if (this.exitTypeDescriptor == null && conditionDescriptor != null && ifNullValueDescriptor != null) {
|
||||
|
||||
@@ -346,6 +346,7 @@ public class Indexer extends SpelNodeImpl {
|
||||
|
||||
Label skipIfNull = null;
|
||||
if (isNullSafe()) {
|
||||
CodeFlow.insertOptionalUnwrapIfNecessary(mv, descriptor);
|
||||
mv.visitInsn(DUP);
|
||||
skipIfNull = new Label();
|
||||
Label continueLabel = new Label();
|
||||
|
||||
+17
@@ -77,6 +77,15 @@ public class MethodReference extends SpelNodeImpl {
|
||||
|
||||
private @Nullable Character originalPrimitiveExitTypeDescriptor;
|
||||
|
||||
/**
|
||||
* Tracks whether an {@link Optional} was unwrapped in
|
||||
* {@link #getValueInternal(EvaluationContext, Object, TypeDescriptor, Object[])}
|
||||
* using the null-safe operator and therefore needs to be unwrapped in a
|
||||
* compiled expression.
|
||||
* @since 7.1
|
||||
*/
|
||||
private boolean unwrapOptional;
|
||||
|
||||
private volatile @Nullable CachedMethodExecutor cachedExecutor;
|
||||
|
||||
|
||||
@@ -133,6 +142,7 @@ public class MethodReference extends SpelNodeImpl {
|
||||
List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments);
|
||||
Optional<?> fallbackOptionalTarget = null;
|
||||
boolean isEmptyOptional = false;
|
||||
this.unwrapOptional = false;
|
||||
|
||||
if (isNullSafe()) {
|
||||
if (target == null) {
|
||||
@@ -142,6 +152,7 @@ public class MethodReference extends SpelNodeImpl {
|
||||
if (optional.isPresent()) {
|
||||
target = optional.get();
|
||||
fallbackOptionalTarget = optional;
|
||||
this.unwrapOptional = true;
|
||||
}
|
||||
else {
|
||||
isEmptyOptional = true;
|
||||
@@ -193,6 +204,9 @@ public class MethodReference extends SpelNodeImpl {
|
||||
if (searchResult.methodExecutor != null) {
|
||||
executorToUse = searchResult.methodExecutor;
|
||||
targetToUse = fallbackOptionalTarget;
|
||||
// If we end up using a method on the original Optional instance,
|
||||
// we don't need to unwrap the Optional in the compiled expression.
|
||||
this.unwrapOptional = false;
|
||||
}
|
||||
}
|
||||
// If we got this far, that means we failed to find an executor for both the
|
||||
@@ -384,6 +398,9 @@ public class MethodReference extends SpelNodeImpl {
|
||||
|
||||
Label skipIfNull = null;
|
||||
if (isNullSafe() && (descriptor != null || !isStatic)) {
|
||||
if (this.unwrapOptional) {
|
||||
CodeFlow.insertOptionalUnwrapIfNecessary(mv, descriptor);
|
||||
}
|
||||
skipIfNull = new Label();
|
||||
Label continueLabel = new Label();
|
||||
mv.visitInsn(DUP);
|
||||
|
||||
+16
@@ -72,6 +72,14 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
|
||||
|
||||
private @Nullable String originalPrimitiveExitTypeDescriptor;
|
||||
|
||||
/**
|
||||
* Tracks whether an {@link Optional} was unwrapped in
|
||||
* {@link #readProperty(TypedValue, EvaluationContext, String)} using the
|
||||
* null-safe operator and therefore needs to be unwrapped in a compiled expression.
|
||||
* @since 7.1
|
||||
*/
|
||||
private boolean unwrapOptional;
|
||||
|
||||
private volatile @Nullable PropertyAccessor cachedReadAccessor;
|
||||
|
||||
private volatile @Nullable PropertyAccessor cachedWriteAccessor;
|
||||
@@ -196,6 +204,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
|
||||
Object target = originalTarget;
|
||||
Optional<?> fallbackOptionalTarget = null;
|
||||
boolean isEmptyOptional = false;
|
||||
this.unwrapOptional = false;
|
||||
|
||||
if (isNullSafe()) {
|
||||
if (target == null) {
|
||||
@@ -205,6 +214,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
|
||||
if (optional.isPresent()) {
|
||||
target = optional.get();
|
||||
fallbackOptionalTarget = optional;
|
||||
this.unwrapOptional = true;
|
||||
}
|
||||
else {
|
||||
isEmptyOptional = true;
|
||||
@@ -249,6 +259,9 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
|
||||
evalContext, fallbackOptionalTarget, name);
|
||||
}
|
||||
this.cachedReadAccessor = accessor;
|
||||
// If we end up using a property on the original Optional instance,
|
||||
// we don't need to unwrap the Optional in the compiled expression.
|
||||
this.unwrapOptional = false;
|
||||
return accessor.read(evalContext, fallbackOptionalTarget, name);
|
||||
}
|
||||
}
|
||||
@@ -357,6 +370,9 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
|
||||
|
||||
Label skipIfNull = null;
|
||||
if (isNullSafe()) {
|
||||
if (this.unwrapOptional) {
|
||||
CodeFlow.insertOptionalUnwrapIfNecessary(mv, cf.lastDescriptor());
|
||||
}
|
||||
mv.visitInsn(DUP);
|
||||
skipIfNull = new Label();
|
||||
Label continueLabel = new Label();
|
||||
|
||||
+6
-2
@@ -354,14 +354,18 @@ class OptionalNullSafetyTests {
|
||||
}
|
||||
|
||||
|
||||
record Jedi(String name) {
|
||||
public record Jedi(String name) {
|
||||
|
||||
public static Jedi from(String name) {
|
||||
return new Jedi(name);
|
||||
}
|
||||
|
||||
public String salutation(String salutation) {
|
||||
return salutation + " " + this.name;
|
||||
}
|
||||
}
|
||||
|
||||
static class Service {
|
||||
public static class Service {
|
||||
|
||||
public Optional<Jedi> findJediByName(@Nullable String name) {
|
||||
if (name == null) {
|
||||
|
||||
+257
@@ -29,6 +29,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -37,6 +38,7 @@ import java.util.stream.Stream;
|
||||
import example.Color;
|
||||
import example.FruitMap;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
@@ -49,6 +51,8 @@ import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.IndexAccessor;
|
||||
import org.springframework.expression.TypedValue;
|
||||
import org.springframework.expression.spel.OptionalNullSafetyTests.Jedi;
|
||||
import org.springframework.expression.spel.OptionalNullSafetyTests.Service;
|
||||
import org.springframework.expression.spel.ast.CompoundExpression;
|
||||
import org.springframework.expression.spel.ast.InlineList;
|
||||
import org.springframework.expression.spel.ast.OpLT;
|
||||
@@ -62,6 +66,7 @@ import org.springframework.expression.spel.support.ReflectiveIndexAccessor;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.expression.spel.testdata.PersonInOtherPackage;
|
||||
import org.springframework.expression.spel.testresources.Person;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
@@ -6294,6 +6299,258 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 7.1
|
||||
*/
|
||||
@Nested
|
||||
class OptionalNullSafeTests { // gh-36330
|
||||
|
||||
private final StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
|
||||
|
||||
@Test
|
||||
void accessOptionalPropertyOnEmptyOptionalViaNullSafeOperator() {
|
||||
String exitDescriptor = CodeFlow.toDescriptor(Boolean.class);
|
||||
context.setVariable("service", new Service());
|
||||
expression = parser.parseExpression("#service.findJediByName('')?.present");
|
||||
|
||||
assertThat(expression.getValue(context, Boolean.class)).isFalse();
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context, Boolean.class)).isFalse();
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessOptionalPropertyOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
String exitDescriptor = CodeFlow.toDescriptor(Boolean.class);
|
||||
context.setVariable("service", new Service());
|
||||
expression = parser.parseExpression("#service.findJediByName('Yoda')?.present");
|
||||
|
||||
assertThat(expression.getValue(context, Boolean.class)).isTrue();
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context, Boolean.class)).isTrue();
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeOptionalMethodOnEmptyOptionalViaNullSafeOperator() {
|
||||
// Object instead of String, since Optional#orElse returns T.
|
||||
String exitDescriptor = CodeFlow.toDescriptor(Object.class);
|
||||
context.setVariable("service", new Service());
|
||||
expression = parser.parseExpression("#service.findJediByName('')?.orElse('Luke')");
|
||||
|
||||
assertThat(expression.getValue(context)).isEqualTo("Luke");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("Luke");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeOptionalMethodOnNonEmptyOptionalViaNullSafeOperator() {
|
||||
// Object instead of Jedi, since Optional#orElse returns T.
|
||||
String exitDescriptor = CodeFlow.toDescriptor(Object.class);
|
||||
context.setVariable("service", new Service());
|
||||
expression = parser.parseExpression("#service.findJediByName('Yoda')?.orElse('Luke')");
|
||||
|
||||
assertThat(expression.getValue(context)).isEqualTo(new Jedi("Yoda"));
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo(new Jedi("Yoda"));
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessPropertyOnOptionalViaNullSafeOperator() {
|
||||
String exitDescriptor = CodeFlow.toDescriptor(String.class);
|
||||
expression = parser.parseExpression("#jedi?.name");
|
||||
|
||||
// 1) Start with empty Optional
|
||||
context.setVariable("jedi", Optional.empty());
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
// Cannot compile before the "name" property type is known.
|
||||
assertThat(getAst().getExitDescriptor()).isNull();
|
||||
assertCannotCompile(expression);
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
|
||||
// 2) Switch to non-empty Optional
|
||||
context.setVariable("jedi", Optional.of(new Jedi("Yoda")));
|
||||
assertThat(expression.getValue(context)).isEqualTo("Yoda");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("Yoda");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
|
||||
// 3) Switch back to empty Optional
|
||||
context.setVariable("jedi", Optional.empty());
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
// Exit descriptor hasn't changed.
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeMethodOnOptionalViaNullSafeOperator() {
|
||||
String exitDescriptor = CodeFlow.toDescriptor(String.class);
|
||||
expression = parser.parseExpression("#jedi?.salutation('Master')");
|
||||
|
||||
// 1) Start with empty Optional
|
||||
context.setVariable("jedi", Optional.empty());
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
// Cannot compile before the "salutation()" return type is known.
|
||||
assertThat(getAst().getExitDescriptor()).isNull();
|
||||
assertCannotCompile(expression);
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
|
||||
// 2) Switch to non-empty Optional
|
||||
context.setVariable("jedi", Optional.of(new Jedi("Yoda")));
|
||||
assertThat(expression.getValue(context)).isEqualTo("Master Yoda");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("Master Yoda");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
|
||||
// 3) Switch back to empty Optional
|
||||
context.setVariable("jedi", Optional.empty());
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
// Exit descriptor hasn't changed.
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessIndexOnOptionalViaNullSafeOperator() {
|
||||
// Object instead of String, since Indexer.CollectionIndexingValueRef
|
||||
// sets the exit descriptor to Object for any Collection.
|
||||
String exitDescriptor = CodeFlow.toDescriptor(Object.class);
|
||||
expression = parser.parseExpression("#fruits?.[1]");
|
||||
|
||||
// 1) Start with empty Optional
|
||||
context.setVariable("fruits", Optional.empty());
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
// Cannot compile before the indexed value type is known.
|
||||
assertThat(getAst().getExitDescriptor()).isNull();
|
||||
assertCannotCompile(expression);
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
|
||||
// 2) Switch to non-empty Optional
|
||||
context.setVariable("fruits", Optional.of(List.of("banana", "lemon", "mango")));
|
||||
assertThat(expression.getValue(context)).isEqualTo("lemon");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("lemon");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
|
||||
// 3) Switch back to empty Optional
|
||||
context.setVariable("fruits", Optional.empty());
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
// Exit descriptor hasn't changed.
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isNull();
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 7.1
|
||||
*/
|
||||
@Nested
|
||||
class OptionalElvisTests { // gh-36330
|
||||
|
||||
private final StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
|
||||
@BeforeEach
|
||||
void configureContext() {
|
||||
context.setVariable("service", new Service());
|
||||
context.registerFunction("jedi", ClassUtils.getMethod(Jedi.class, "from", String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void elvisOperatorOnEmptyOptional() {
|
||||
// Object instead of Optional or String, since the Elvis operator uses
|
||||
// "the easiest to compute common supertype" if the types for the
|
||||
// LHS and RHS do not match.
|
||||
String exitDescriptor = CodeFlow.toDescriptor(Object.class);
|
||||
expression = parser.parseExpression("#service.findJediByName('') ?: 'unknown'");
|
||||
|
||||
assertThat(expression.getValue(context)).isEqualTo("unknown");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("unknown");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
void elvisOperatorOnNonEmptyOptionalWithNonMatchingElseType() {
|
||||
// Object instead of Jedi or String, since the Elvis operator uses
|
||||
// "the easiest to compute common supertype" if the types for the
|
||||
// LHS and RHS do not match.
|
||||
String exitDescriptor = CodeFlow.toDescriptor(Object.class);
|
||||
expression = parser.parseExpression("#service.findJediByName('Yoda') ?: 'unknown'");
|
||||
|
||||
assertThat(expression.getValue(context)).isEqualTo(new Jedi("Yoda"));
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo(new Jedi("Yoda"));
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
void elvisOperatorOnNonEmptyOptionalWithMatchingElseType() {
|
||||
// Jedi, since the known types for the LHS and RHS eventually match.
|
||||
String exitDescriptor = CodeFlow.toDescriptor(Jedi.class);
|
||||
expression = parser.parseExpression("#service.findJediByName(#name) ?: #jedi('unknown')");
|
||||
|
||||
// 1) Start with non-empty Optional
|
||||
context.setVariable("name", "Yoda");
|
||||
assertThat(expression.getValue(context)).isEqualTo(new Jedi("Yoda"));
|
||||
// Cannot compile before the types for the LHS and RHS are known.
|
||||
assertThat(getAst().getExitDescriptor()).isNull();
|
||||
assertCannotCompile(expression);
|
||||
assertThat(getAst().getExitDescriptor()).isNull();
|
||||
|
||||
// 2) Switch to empty Optional
|
||||
context.setVariable("name", "");
|
||||
assertThat(expression.getValue(context)).isEqualTo(new Jedi("unknown"));
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
|
||||
// 3) Switch back to non-empty Optional
|
||||
context.setVariable("name", "Luke");
|
||||
assertThat(expression.getValue(context)).isEqualTo(new Jedi("Luke"));
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
assertCanCompile(expression);
|
||||
assertIsCompiled(expression);
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class MethodVisibilityTests {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user