Support single String argument for varargs invocations in SpEL

Prior to this commit, the Spring Expression Language (SpEL) incorrectly
split single String arguments by comma for Object... varargs method and
constructor invocations.

This commit addresses this by checking if the single argument type is
already "assignable" to the varargs component type instead of "equal"
to the varargs component type.

Closes gh-33013
This commit is contained in:
Sam Brannen
2024-07-10 11:51:55 +02:00
parent a4fcd301f2
commit c55c64427c
6 changed files with 122 additions and 11 deletions

View File

@@ -293,6 +293,46 @@ class MethodInvocationTests extends AbstractExpressionTests {
evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class);
}
@Test // gh-33013
void testVarargsWithObjectArrayType() {
// Calling 'public String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
// No var-args and no conversion necessary
evaluate("formatObjectVarargs('x')", "x", String.class);
// No var-args but conversion necessary
evaluate("formatObjectVarargs(9)", "9", String.class);
// No conversion necessary
evaluate("formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
evaluate("formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
evaluate("formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
evaluate("formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
evaluate("formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
evaluate("formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
evaluate("formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
evaluate("formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class);
evaluate("formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class);
evaluate("formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class);
evaluate("formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class);
evaluate("formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
// Conversion necessary
evaluate("formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
evaluate("formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
// Individual string contains a comma with multiple varargs arguments
evaluate("formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
evaluate("formatObjectVarargs('foo -> %s %s', 'bar', ',baz')", "foo -> bar ,baz", String.class);
evaluate("formatObjectVarargs('foo -> %s %s', 'bar,', 'baz')", "foo -> bar, baz", String.class);
// Individual string contains a comma with single varargs argument.
evaluate("formatObjectVarargs('foo -> %s', ',')", "foo -> ,", String.class);
evaluate("formatObjectVarargs('foo -> %s', ',bar')", "foo -> ,bar", String.class);
evaluate("formatObjectVarargs('foo -> %s', 'bar,')", "foo -> bar,", String.class);
evaluate("formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
}
@Test
void testVarargsOptionalInvocation() {
// Calling 'public String optionalVarargsMethod(Optional<String>... values)'

View File

@@ -69,6 +69,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.InstanceOfAssertFactories.list;
/**
* Reproduction tests cornering various reported SpEL issues.
@@ -1436,13 +1437,20 @@ class SpelReproTests extends AbstractExpressionTests {
assertThat(expression.getValue(new NamedUser())).isEqualTo(NamedUser.class.getName());
}
@Test
void SPR12522() {
@Test // gh-17127, SPR-12522
void arraysAsListWithNoArguments() {
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("T(java.util.Arrays).asList()");
List<?> value = expression.getValue(List.class);
assertThat(value).isEmpty();
}
@Test // gh-33013
void arraysAsListWithSingleEmptyStringArgument() {
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("T(java.util.Arrays).asList('')");
Object value = expression.getValue();
assertThat(value).isInstanceOf(List.class);
assertThat(((List<?>) value)).isEmpty();
List<?> value = expression.getValue(List.class);
assertThat(value).asInstanceOf(list(String.class)).containsExactly("");
}
@Test

View File

@@ -100,7 +100,12 @@ class TestScenarioCreator {
MethodHandle messageStaticFullyBound = messageStaticPartiallyBound
.bindTo(new String[] { "prerecorded", "3", "Oh Hello World", "ignored"});
testContext.registerFunction("messageStaticBound", messageStaticFullyBound);
}
// #formatObjectVarargs(format, args...)
MethodHandle formatObjectVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
}
/**
* Register some variables that can be referenced from the tests
@@ -154,4 +159,8 @@ class TestScenarioCreator {
return template.formatted((Object[]) args);
}
public static String formatObjectVarargs(String format, Object... args) {
return String.format(format, args);
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.expression.spel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -101,6 +102,54 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#varargsFunction2(9,'a',null,'b')", "9-[a, null, b]", String.class);
}
@Disabled("Disabled until bugs are reported and fixed")
@Test
void functionWithVarargsViaMethodHandle_CurrentlyFailing() {
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
// No var-args and no conversion necessary
evaluate("#formatObjectVarargs('x')", "x", String.class);
// No var-args but conversion necessary
evaluate("#formatObjectVarargs(9)", "9", String.class);
// No conversion necessary
evaluate("#formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
}
@Test // gh-33013
void functionWithVarargsViaMethodHandle() {
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
// No conversion necessary
evaluate("#formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
// Conversion necessary
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
// Individual string contains a comma with multiple varargs arguments
evaluate("#formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
evaluate("#formatObjectVarargs('foo -> %s %s', 'bar', ',baz')", "foo -> bar ,baz", String.class);
evaluate("#formatObjectVarargs('foo -> %s %s', 'bar,', 'baz')", "foo -> bar, baz", String.class);
// Individual string contains a comma with single varargs argument.
evaluate("#formatObjectVarargs('foo -> %s', ',')", "foo -> ,", String.class);
evaluate("#formatObjectVarargs('foo -> %s', ',bar')", "foo -> ,bar", String.class);
evaluate("#formatObjectVarargs('foo -> %s', 'bar,')", "foo -> bar,", String.class);
evaluate("#formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
}
@Test
void functionMethodMustBeStatic() throws Exception {
SpelExpressionParser parser = new SpelExpressionParser();

View File

@@ -217,6 +217,11 @@ public class Inventor {
return str1 + "-" + String.join("-", strings);
}
public String formatObjectVarargs(String format, Object... args) {
return String.format(format, args);
}
public Inventor(String... strings) {
if (strings.length > 0) {
this.name = strings[0];