Support compilation of null-safe index operations in SpEL

See gh-29847
This commit is contained in:
Sam Brannen
2024-03-22 17:44:37 +01:00
parent d2bd0d5716
commit 38c473fd05
2 changed files with 251 additions and 10 deletions

View File

@@ -725,6 +725,198 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
}
@Nested
class NullSafeIndexTests { // gh-29847
private final RootContextWithIndexedProperties rootContext = new RootContextWithIndexedProperties();
private final StandardEvaluationContext context = new StandardEvaluationContext(rootContext);
@Test
void nullSafeIndexIntoPrimitiveIntArray() {
expression = parser.parseExpression("intArray?.[0]");
// Cannot compile before the array type is known.
assertThat(expression.getValue(context)).isNull();
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isNull();
rootContext.intArray = new int[] { 8, 9, 10 };
assertThat(expression.getValue(context)).isEqualTo(8);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(8);
// Normally we would expect the exit type descriptor to be "I" for an
// element of an int[]. However, with null-safe indexing support the
// only way for it to evaluate to null is to box the 'int' to an 'Integer'.
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer");
// Null-safe support should have been compiled once the array type is known.
rootContext.intArray = null;
assertThat(expression.getValue(context)).isNull();
assertCanCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer");
}
@Test
void nullSafeIndexIntoNumberArray() {
expression = parser.parseExpression("numberArray?.[0]");
// Cannot compile before the array type is known.
assertThat(expression.getValue(context)).isNull();
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isNull();
rootContext.numberArray = new Number[] { 8, 9, 10 };
assertThat(expression.getValue(context)).isEqualTo(8);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(8);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number");
// Null-safe support should have been compiled once the array type is known.
rootContext.numberArray = null;
assertThat(expression.getValue(context)).isNull();
assertCanCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number");
}
@Test
void nullSafeIndexIntoList() {
expression = parser.parseExpression("list?.[0]");
// Cannot compile before the list type is known.
assertThat(expression.getValue(context)).isNull();
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isNull();
rootContext.list = List.of(42);
assertThat(expression.getValue(context)).isEqualTo(42);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(42);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// Null-safe support should have been compiled once the list type is known.
rootContext.list = null;
assertThat(expression.getValue(context)).isNull();
assertCanCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void nullSafeIndexIntoSetCannotBeCompiled() {
expression = parser.parseExpression("set?.[0]");
assertThat(expression.getValue(context)).isNull();
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isNull();
rootContext.set = Set.of(42);
assertThat(expression.getValue(context)).isEqualTo(42);
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(42);
assertThat(getAst().getExitDescriptor()).isNull();
}
@Test
void nullSafeIndexIntoStringCannotBeCompiled() {
expression = parser.parseExpression("string?.[0]");
assertThat(expression.getValue(context)).isNull();
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isNull();
rootContext.string = "XYZ";
assertThat(expression.getValue(context)).isEqualTo("X");
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("X");
assertThat(getAst().getExitDescriptor()).isNull();
}
@Test
void nullSafeIndexIntoMap() {
expression = parser.parseExpression("map?.['enigma']");
// Cannot compile before the map type is known.
assertThat(expression.getValue(context)).isNull();
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isNull();
rootContext.map = Map.of("enigma", 42);
assertThat(expression.getValue(context)).isEqualTo(42);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(42);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// Null-safe support should have been compiled once the map type is known.
rootContext.map = null;
assertThat(expression.getValue(context)).isNull();
assertCanCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void nullSafeIndexIntoObjectViaPrimitiveProperty() {
expression = parser.parseExpression("person?.['age']");
// Cannot compile before the Person type is known.
assertThat(expression.getValue(context)).isNull();
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isNull();
rootContext.person = new Person("Jane");
rootContext.person.setAge(42);
assertThat(expression.getValue(context)).isEqualTo(42);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(42);
// Normally we would expect the exit type descriptor to be "I" for
// an int. However, with null-safe indexing support the only way
// for it to evaluate to null is to box the 'int' to an 'Integer'.
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer");
// Null-safe support should have been compiled once the Person type is known.
rootContext.person = null;
assertThat(expression.getValue(context)).isNull();
assertCanCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer");
}
@Test
void nullSafeIndexIntoObjectViaStringProperty() {
expression = parser.parseExpression("person?.['name']");
// Cannot compile before the Person type is known.
assertThat(expression.getValue(context)).isNull();
assertCannotCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isNull();
rootContext.person = new Person("Jane");
assertThat(expression.getValue(context)).isEqualTo("Jane");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("Jane");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
// Null-safe support should have been compiled once the Person type is known.
rootContext.person = null;
assertThat(expression.getValue(context)).isNull();
assertCanCompile(expression);
assertThat(expression.getValue(context)).isNull();
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
}
@Nested
class PropertyVisibilityTests {
@@ -6736,4 +6928,15 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
}
}
// Must be public with public fields/properties.
public static class RootContextWithIndexedProperties {
public int[] intArray;
public Number[] numberArray;
public List<Integer> list;
public Set<Integer> set;
public String string;
public Map<String, Integer> map;
public Person person;
}
}