Skip to content

Commit 12727a2

Browse files
LeMikaelFsbrannen
authored andcommitted
Support compilation of varargs invocations in SpEL for array subtypes
This commit introduces support for compiling SpEL expressions that contain varargs invocations where the supplied array is a subtype of the declared varargs array type. See gh-32804
1 parent 29bb7b9 commit 12727a2

File tree

2 files changed

+155
-64
lines changed

2 files changed

+155
-64
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616

1717
package org.springframework.expression.spel.ast;
1818

19-
import java.lang.reflect.Constructor;
20-
import java.lang.reflect.Member;
21-
import java.lang.reflect.Method;
19+
import java.lang.reflect.Executable;
20+
import java.util.Objects;
2221
import java.util.function.Supplier;
2322

2423
import org.springframework.asm.MethodVisitor;
@@ -214,62 +213,67 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException
214213
* and if it is then the argument values will be appropriately packaged into an array.
215214
* @param mv the method visitor where code should be generated
216215
* @param cf the current codeflow
217-
* @param member the method or constructor for which arguments are being set up
216+
* @param executable the method or constructor for which arguments are being set up
218217
* @param arguments the expression nodes for the expression supplied argument values
219218
*/
220-
protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Member member, SpelNodeImpl[] arguments) {
221-
String[] paramDescriptors = null;
222-
boolean isVarargs = false;
223-
if (member instanceof Constructor<?> ctor) {
224-
paramDescriptors = CodeFlow.toDescriptors(ctor.getParameterTypes());
225-
isVarargs = ctor.isVarArgs();
226-
}
227-
else { // Method
228-
Method method = (Method)member;
229-
paramDescriptors = CodeFlow.toDescriptors(method.getParameterTypes());
230-
isVarargs = method.isVarArgs();
231-
}
232-
if (isVarargs) {
219+
protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Executable executable, SpelNodeImpl[] arguments) {
220+
String[] paramDescriptors = CodeFlow.toDescriptors(executable.getParameterTypes());
221+
int paramCount = paramDescriptors.length;
222+
223+
if (executable.isVarArgs()) {
233224
// The final parameter may or may not need packaging into an array, or nothing may
234225
// have been passed to satisfy the varargs and so something needs to be built.
235-
int p = 0; // Current supplied argument being processed
236-
int childCount = arguments.length;
237226

238227
// Fulfill all the parameter requirements except the last one
239-
for (p = 0; p < paramDescriptors.length - 1; p++) {
240-
cf.generateCodeForArgument(mv, arguments[p], paramDescriptors[p]);
228+
for (int i = 0; i < paramCount - 1; i++) {
229+
cf.generateCodeForArgument(mv, arguments[i], paramDescriptors[i]);
241230
}
242231

243-
SpelNodeImpl lastChild = (childCount == 0 ? null : arguments[childCount - 1]);
244-
String arrayType = paramDescriptors[paramDescriptors.length - 1];
245-
// Determine if the final passed argument is already suitably packaged in array
246-
// form to be passed to the method
247-
if (lastChild != null && arrayType.equals(lastChild.getExitDescriptor())) {
248-
cf.generateCodeForArgument(mv, lastChild, paramDescriptors[p]);
249-
}
250-
else {
251-
arrayType = arrayType.substring(1); // trim the leading '[', may leave other '['
252-
// build array big enough to hold remaining arguments
253-
CodeFlow.insertNewArrayCode(mv, childCount - p, arrayType);
254-
// Package up the remaining arguments into the array
255-
int arrayindex = 0;
256-
while (p < childCount) {
257-
SpelNodeImpl child = arguments[p];
232+
int argCount = arguments.length;
233+
String varargsType = paramDescriptors[paramCount - 1];
234+
String varargsComponentType = varargsType.substring(1); // trim the leading '[', may leave other '['
235+
236+
if (needsVarargsArrayWrapping(arguments, paramDescriptors)) {
237+
// Package up the remaining arguments into an array
238+
CodeFlow.insertNewArrayCode(mv, argCount - paramCount + 1, varargsComponentType);
239+
for (int argIndex = paramCount - 1, arrayIndex = 0; argIndex < argCount; argIndex++, arrayIndex++) {
240+
SpelNodeImpl child = arguments[argIndex];
258241
mv.visitInsn(DUP);
259-
CodeFlow.insertOptimalLoad(mv, arrayindex++);
260-
cf.generateCodeForArgument(mv, child, arrayType);
261-
CodeFlow.insertArrayStore(mv, arrayType);
262-
p++;
242+
CodeFlow.insertOptimalLoad(mv, arrayIndex);
243+
cf.generateCodeForArgument(mv, child, varargsComponentType);
244+
CodeFlow.insertArrayStore(mv, varargsComponentType);
263245
}
264246
}
247+
else if (varargsType.equals(arguments[argCount - 1].getExitDescriptor())) {
248+
// varargs type matches type of last argument
249+
cf.generateCodeForArgument(mv, arguments[argCount - 1], paramDescriptors[paramCount - 1]);
250+
}
251+
else {
252+
// last argument is an array and varargs component type is a supertype of argument component type
253+
cf.generateCodeForArgument(mv, arguments[argCount - 1], varargsComponentType);
254+
}
265255
}
266256
else {
267-
for (int i = 0; i < paramDescriptors.length;i++) {
257+
for (int i = 0; i < paramCount; i++) {
268258
cf.generateCodeForArgument(mv, arguments[i], paramDescriptors[i]);
269259
}
270260
}
271261
}
272262

263+
private static boolean needsVarargsArrayWrapping(SpelNodeImpl[] arguments, String[] paramDescriptors) {
264+
if (arguments.length == 0) {
265+
return true;
266+
}
267+
268+
String lastExitTypeDescriptor = arguments[arguments.length - 1].exitTypeDescriptor;
269+
String lastParamDescriptor = paramDescriptors[paramDescriptors.length - 1];
270+
return countLeadingOpeningBrackets(Objects.requireNonNull(lastExitTypeDescriptor)) != countLeadingOpeningBrackets(lastParamDescriptor);
271+
}
272+
273+
private static long countLeadingOpeningBrackets(String lastExitTypeDescriptor) {
274+
return lastExitTypeDescriptor.chars().takeWhile(c -> c == '[').count();
275+
}
276+
273277
/**
274278
* Ask an argument to generate its bytecode and then follow it up
275279
* with any boxing/unboxing/checkcasting to ensure it matches the expected parameter descriptor.

spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
import java.util.HashSet;
2929
import java.util.List;
3030
import java.util.Map;
31+
import java.util.Objects;
3132
import java.util.Set;
3233
import java.util.StringTokenizer;
34+
import java.util.stream.Collectors;
3335
import java.util.stream.Stream;
3436

3537
import example.Color;
@@ -2267,6 +2269,12 @@ public static String concat(String a, String b) {
22672269
return a+b;
22682270
}
22692271

2272+
public static String concat2(Object... args) {
2273+
return Arrays.stream(args)
2274+
.map(Objects::toString)
2275+
.collect(Collectors.joining());
2276+
}
2277+
22702278
public static String join(String...strings) {
22712279
StringBuilder buf = new StringBuilder();
22722280
for (String string: strings) {
@@ -2279,7 +2287,7 @@ public static String join(String...strings) {
22792287
void compiledExpressionShouldWorkWhenUsingCustomFunctionWithVarargs() throws Exception {
22802288
StandardEvaluationContext context;
22812289

2282-
// Here the target method takes Object... and we are passing a string
2290+
// single string argument
22832291
expression = parser.parseExpression("#doFormat('hey %s', 'there')");
22842292
context = new StandardEvaluationContext();
22852293
context.registerFunction("doFormat",
@@ -2291,6 +2299,7 @@ void compiledExpressionShouldWorkWhenUsingCustomFunctionWithVarargs() throws Exc
22912299
assertCanCompile(expression);
22922300
assertThat(expression.getValue(String.class)).isEqualTo("hey there");
22932301

2302+
// single string argument from root array access
22942303
expression = parser.parseExpression("#doFormat([0], 'there')");
22952304
context = new StandardEvaluationContext(new Object[] {"hey %s"});
22962305
context.registerFunction("doFormat",
@@ -2302,6 +2311,7 @@ void compiledExpressionShouldWorkWhenUsingCustomFunctionWithVarargs() throws Exc
23022311
assertCanCompile(expression);
23032312
assertThat(expression.getValue(String.class)).isEqualTo("hey there");
23042313

2314+
// single string from variable
23052315
expression = parser.parseExpression("#doFormat([0], #arg)");
23062316
context = new StandardEvaluationContext(new Object[] {"hey %s"});
23072317
context.registerFunction("doFormat",
@@ -2313,13 +2323,29 @@ void compiledExpressionShouldWorkWhenUsingCustomFunctionWithVarargs() throws Exc
23132323
assertThat(((SpelExpression) expression).getAST().isCompilable()).isTrue();
23142324
assertCanCompile(expression);
23152325
assertThat(expression.getValue(String.class)).isEqualTo("hey there");
2326+
2327+
// string array argument
2328+
expression = parser.parseExpression("#doFormat('hey %s', #arg)");
2329+
context = new StandardEvaluationContext();
2330+
context.registerFunction("doFormat",
2331+
DelegatingStringFormat.class.getDeclaredMethod("format", String.class, Object[].class));
2332+
context.setVariable("arg", new String[] { "there" });
2333+
((SpelExpression) expression).setEvaluationContext(context);
2334+
assertThat(expression.getValue(String.class)).isEqualTo("hey there");
2335+
assertThat(((SpelExpression) expression).getAST().isCompilable()).isTrue();
2336+
assertCanCompile(expression);
2337+
assertThat(expression.getValue(String.class)).isEqualTo("hey there");
23162338
}
23172339

23182340
@Test
23192341
void functionReference() throws Exception {
23202342
EvaluationContext ctx = new StandardEvaluationContext();
23212343
Method m = getClass().getDeclaredMethod("concat", String.class, String.class);
2322-
ctx.setVariable("concat",m);
2344+
ctx.setVariable("concat", m);
2345+
Method m2 = getClass().getDeclaredMethod("concat2", Object[].class);
2346+
ctx.setVariable("concat2", m2);
2347+
Method m3 = getClass().getDeclaredMethod("join", String[].class);
2348+
ctx.setVariable("join", m3);
23232349

23242350
expression = parser.parseExpression("#concat('a','b')");
23252351
assertThat(expression.getValue(ctx)).isEqualTo("ab");
@@ -2331,6 +2357,20 @@ void functionReference() throws Exception {
23312357
assertCanCompile(expression);
23322358
assertThat(expression.getValue(ctx)).isEqualTo('b');
23332359

2360+
// varargs
2361+
expression = parser.parseExpression("#join(#stringArray)");
2362+
ctx.setVariable("stringArray", new String[] { "a", "b", "c" });
2363+
assertThat(expression.getValue(ctx)).isEqualTo("abc");
2364+
assertCanCompile(expression);
2365+
assertThat(expression.getValue(ctx)).isEqualTo("abc");
2366+
2367+
// varargs with argument component type that is a subtype of the varargs component type.
2368+
expression = parser.parseExpression("#concat2(#stringArray)");
2369+
ctx.setVariable("stringArray", new String[] { "a", "b", "c" });
2370+
assertThat(expression.getValue(ctx)).isEqualTo("abc");
2371+
assertCanCompile(expression);
2372+
assertThat(expression.getValue(ctx)).isEqualTo("abc");
2373+
23342374
expression = parser.parseExpression("#concat(#a,#b)");
23352375
ctx.setVariable("a", "foo");
23362376
ctx.setVariable("b", "bar");
@@ -2524,12 +2564,11 @@ void functionReferenceVarargs_SPR12359() throws Exception {
25242564
assertCanCompile(expression);
25252565
assertThat(expression.getValue(context, new SomeCompareMethod2()).toString()).isEqualTo("xyz");
25262566

2527-
// TODO Determine why the String[] is passed as the first element of the Object... varargs array instead of the entire varargs array.
2528-
// expression = parser.parseExpression("#append2(#stringArray)");
2529-
// assertThat(expression.getValue(context)).hasToString("xyz");
2530-
// assertThat(((SpelExpression) expression).getAST().isCompilable()).isTrue();
2531-
// assertCanCompile(expression);
2532-
// assertThat(expression.getValue(context)).hasToString("xyz");
2567+
expression = parser.parseExpression("#append2(#stringArray)");
2568+
assertThat(expression.getValue(context)).hasToString("xyz");
2569+
assertThat(((SpelExpression) expression).getAST().isCompilable()).isTrue();
2570+
assertCanCompile(expression);
2571+
assertThat(expression.getValue(context)).hasToString("xyz");
25332572

25342573
expression = parser.parseExpression("#sum(1,2,3)");
25352574
assertThat(expression.getValue(context)).isEqualTo(6);
@@ -4878,6 +4917,25 @@ void constructorReference() {
48784917
tc8 = (TestClass8) o;
48794918
assertThat(tc8.i).isEqualTo(42);
48804919

4920+
// varargs
4921+
expression = parser.parseExpression("new " + testclass8 + "(#root)");
4922+
Object[] objectArray = { "a", "b", "c" };
4923+
assertThat(expression.getValue(objectArray).getClass().getName()).isEqualTo(testclass8);
4924+
assertCanCompile(expression);
4925+
o = expression.getValue(objectArray);
4926+
assertThat(o.getClass().getName()).isEqualTo(testclass8);
4927+
tc8 = (TestClass8) o;
4928+
assertThat(tc8.args).containsExactly("a", "b", "c");
4929+
4930+
// varargs with argument component type that is a subtype of the varargs component type.
4931+
expression = parser.parseExpression("new " + testclass8 + "(#root)");
4932+
assertThat(expression.getValue(objectArray).getClass().getName()).isEqualTo(testclass8);
4933+
assertCanCompile(expression);
4934+
o = expression.getValue(new String[] { "a", "b", "c" });
4935+
assertThat(o.getClass().getName()).isEqualTo(testclass8);
4936+
tc8 = (TestClass8) o;
4937+
assertThat(tc8.args).containsExactly("a", "b", "c");
4938+
48814939
// private class, can't compile it
48824940
String testclass9 = "org.springframework.expression.spel.SpelCompilationCoverageTests$TestClass9";
48834941
expression = parser.parseExpression("new " + testclass9 + "(42)");
@@ -4984,6 +5042,7 @@ void methodReferenceVarargs() {
49845042
assertThat(tc.s).isEqualTo("aaabbbccc");
49855043
tc.reset();
49865044

5045+
// varargs object
49875046
expression = parser.parseExpression("sixteen('aaa','bbb','ccc')");
49885047
assertCannotCompile(expression);
49895048
expression.getValue(tc);
@@ -4994,27 +5053,38 @@ void methodReferenceVarargs() {
49945053
assertThat(tc.s).isEqualTo("aaabbbccc");
49955054
tc.reset();
49965055

5056+
// string array from property in varargs object
49975057
expression = parser.parseExpression("sixteen(seventeen)");
49985058
assertCannotCompile(expression);
49995059
expression.getValue(tc);
50005060
assertThat(tc.s).isEqualTo("aaabbbccc");
50015061
assertCanCompile(expression);
50025062
tc.reset();
5003-
// see TODO below
5004-
// expression.getValue(tc);
5005-
// assertThat(tc.s).isEqualTo("aaabbbccc");
5006-
// tc.reset();
5007-
5008-
// TODO Determine why the String[] is passed as the first element of the Object... varargs array instead of the entire varargs array.
5009-
// expression = parser.parseExpression("sixteen(stringArray)");
5010-
// assertCannotCompile(expression);
5011-
// expression.getValue(tc);
5012-
// assertThat(tc.s).isEqualTo("aaabbbccc");
5013-
// assertCanCompile(expression);
5014-
// tc.reset();
5015-
// expression.getValue(tc);
5016-
// assertThat(tc.s).isEqualTo("aaabbbccc");
5017-
// tc.reset();
5063+
expression.getValue(tc);
5064+
assertThat(tc.s).isEqualTo("aaabbbccc");
5065+
tc.reset();
5066+
5067+
// string array from variable in varargs object
5068+
expression = parser.parseExpression("sixteen(stringArray)");
5069+
assertCannotCompile(expression);
5070+
expression.getValue(tc);
5071+
assertThat(tc.s).isEqualTo("aaabbbccc");
5072+
assertCanCompile(expression);
5073+
tc.reset();
5074+
expression.getValue(tc);
5075+
assertThat(tc.s).isEqualTo("aaabbbccc");
5076+
tc.reset();
5077+
5078+
// string array in varargs object with other parameter
5079+
expression = parser.parseExpression("eighteen('AAA', stringArray)");
5080+
assertCannotCompile(expression);
5081+
expression.getValue(tc);
5082+
assertThat(tc.s).isEqualTo("AAA::aaabbbccc");
5083+
assertCanCompile(expression);
5084+
tc.reset();
5085+
expression.getValue(tc);
5086+
assertThat(tc.s).isEqualTo("AAA::aaabbbccc");
5087+
tc.reset();
50185088

50195089
// varargs int
50205090
expression = parser.parseExpression("twelve(1,2,3)");
@@ -6863,6 +6933,18 @@ public void sixteen(Object... vargs) {
68636933
public String[] seventeen() {
68646934
return new String[] { "aaa", "bbb", "ccc" };
68656935
}
6936+
6937+
public void eighteen(String a, Object... vargs) {
6938+
if (vargs == null) {
6939+
s = a + "::";
6940+
}
6941+
else {
6942+
s = a+"::";
6943+
for (Object varg: vargs) {
6944+
s += varg;
6945+
}
6946+
}
6947+
}
68666948
}
68676949

68686950

@@ -6911,6 +6993,7 @@ public static class TestClass8 {
69116993
public String s;
69126994
public double d;
69136995
public boolean z;
6996+
public Object[] args;
69146997

69156998
public TestClass8(int i, String s, double d, boolean z) {
69166999
this.i = i;
@@ -6926,6 +7009,10 @@ public TestClass8(Integer i) {
69267009
this.i = i;
69277010
}
69287011

7012+
public TestClass8(Object... args) {
7013+
this.args = args;
7014+
}
7015+
69297016
@SuppressWarnings("unused")
69307017
private TestClass8(String a, String b) {
69317018
this.s = a+b;

0 commit comments

Comments
 (0)