Skip to content

Commit

Permalink
Support compilation of varargs invocations in SpEL for array subtypes
Browse files Browse the repository at this point in the history
This commit merges support for compiling SpEL expressions that contain
varargs invocations where the supplied array is a subtype of the
declared varargs array type.

Closes gh-32804
  • Loading branch information
sbrannen committed May 14, 2024
2 parents 9516f87 + 8fe4493 commit 061c13d
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@

package org.springframework.expression.spel.ast;

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.function.Supplier;

import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
import org.springframework.asm.Type;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.common.ExpressionUtils;
Expand All @@ -33,7 +33,9 @@
import org.springframework.expression.spel.SpelNode;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
* The common supertype of all AST nodes in a parsed Spring Expression Language
Expand Down Expand Up @@ -216,20 +218,37 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException
* @param cf the current codeflow
* @param member the method or constructor for which arguments are being set up
* @param arguments the expression nodes for the expression supplied argument values
* @deprecated As of Spring Framework 6.2, in favor of
* {@link #generateCodeForArguments(MethodVisitor, CodeFlow, Executable, SpelNodeImpl[])}
*/
@Deprecated(since = "6.2")
protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Member member, SpelNodeImpl[] arguments) {
String[] paramDescriptors = null;
boolean isVarargs = false;
if (member instanceof Constructor<?> ctor) {
paramDescriptors = CodeFlow.toDescriptors(ctor.getParameterTypes());
isVarargs = ctor.isVarArgs();
if (member instanceof Executable executable) {
generateCodeForArguments(mv, cf, executable, arguments);
}
else { // Method
Method method = (Method)member;
paramDescriptors = CodeFlow.toDescriptors(method.getParameterTypes());
isVarargs = method.isVarArgs();
}
if (isVarargs) {
throw new IllegalArgumentException(
"The supplied member must be an instance of java.lang.reflect.Executable: " + member);
}

/**
* Generate code that handles building the argument values for the specified
* {@link Executable} (method or constructor).
* <p>This method takes into account whether the invoked executable was
* declared to accept varargs, and if it was then the argument values will be
* appropriately packaged into an array.
* @param mv the method visitor where code should be generated
* @param cf the current {@link CodeFlow}
* @param executable the {@link Executable} (method or constructor) for which
* arguments are being set up
* @param arguments the expression nodes for the expression supplied argument
* values
* @since 6.2
*/
protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Executable executable, SpelNodeImpl[] arguments) {
Class<?>[] parameterTypes = executable.getParameterTypes();
String[] paramDescriptors = CodeFlow.toDescriptors(parameterTypes);

if (executable.isVarArgs()) {
// The final parameter may or may not need packaging into an array, or nothing may
// have been passed to satisfy the varargs and so something needs to be built.
int p = 0; // Current supplied argument being processed
Expand All @@ -241,13 +260,18 @@ protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Me
}

SpelNodeImpl lastChild = (childCount == 0 ? null : arguments[childCount - 1]);
String arrayType = paramDescriptors[paramDescriptors.length - 1];
ClassLoader classLoader = executable.getDeclaringClass().getClassLoader();
Class<?> lastChildType = (lastChild != null ?
loadClassForExitDescriptor(lastChild.getExitDescriptor(), classLoader) : null);
Class<?> lastParameterType = parameterTypes[parameterTypes.length - 1];

// Determine if the final passed argument is already suitably packaged in array
// form to be passed to the method
if (lastChild != null && arrayType.equals(lastChild.getExitDescriptor())) {
if (lastChild != null && lastChildType != null && lastParameterType.isAssignableFrom(lastChildType)) {
cf.generateCodeForArgument(mv, lastChild, paramDescriptors[p]);
}
else {
String arrayType = paramDescriptors[paramDescriptors.length - 1];
arrayType = arrayType.substring(1); // trim the leading '[', may leave other '['
// build array big enough to hold remaining arguments
CodeFlow.insertNewArrayCode(mv, childCount - p, arrayType);
Expand All @@ -270,6 +294,19 @@ protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Me
}
}

@Nullable
private static Class<?> loadClassForExitDescriptor(@Nullable String exitDescriptor, ClassLoader classLoader) {
if (!StringUtils.hasText(exitDescriptor)) {
return null;
}
String typeDescriptor = exitDescriptor;
if (typeDescriptor.startsWith("[") || typeDescriptor.startsWith("L")) {
typeDescriptor += ";";
}
String className = Type.getType(typeDescriptor).getClassName();
return ClassUtils.resolveClassName(className, classLoader);
}

/**
* Ask an argument to generate its bytecode and then follow it up
* with any boxing/unboxing/checkcasting to ensure it matches the expected parameter descriptor.
Expand Down

0 comments on commit 061c13d

Please sign in to comment.