Skip to content

Commit

Permalink
Support MethodHandle function invocation with zero varargs in SpEL
Browse files Browse the repository at this point in the history
Prior to this commit, the Spring Expression Language (SpEL) could not
invoke a varargs MethodHandle function with zero variable arguments,
even though the variable arguments are not required. Attempting to do
so resulted in a SpelEvaluationException with an
INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION message.

This commit addresses this by updating the
executeFunctionViaMethodHandle(...) method in FunctionReference so that
it properly checks the required number of arguments for both varargs
and non-varargs MethodHandle invocations.

This commit also improves the error message for varargs invocations
with too few arguments. For example, if the MethodHandle requires at
least 1 argument plus a variable number of additional arguments and 0
arguments were supplied, the error message now states:

"Incorrect number of arguments for function 'myFunc': 0 supplied but function takes 1 or more"

Instead of:

"Incorrect number of arguments for function 'myFunc': 0 supplied but function takes 2"

Closes gh-33190
  • Loading branch information
sbrannen committed Jul 10, 2024
1 parent b5a86de commit a0f5c16
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,28 @@ private TypedValue executeFunctionViaMethodHandle(ExpressionState state, MethodH
int spelParamCount = functionArgs.length;
int declaredParamCount = declaredParams.parameterCount();

// We don't use methodHandle.isVarargsCollector(), because a MethodHandle created via
// MethodHandle#bindTo() is "never a variable-arity method handle, even if the original
// target method handle was." Thus, we merely assume/suspect that varargs are supported
// if the last parameter type is an array.
boolean isSuspectedVarargs = declaredParams.lastParameterType().isArray();

if (spelParamCount < declaredParamCount || (spelParamCount > declaredParamCount && !isSuspectedVarargs)) {
// incorrect number, including more arguments and not a vararg
// perhaps a subset of arguments was provided but the MethodHandle wasn't bound?
if (isSuspectedVarargs) {
if (spelParamCount < declaredParamCount - 1) {
// Varargs, but the number of provided arguments (potentially 0) is insufficient
// for a varargs invocation for the number of declared parameters.
//
// As stated in the Javadoc for MethodHandle#asVarargsCollector(), "the caller
// must supply, at a minimum, N-1 arguments, where N is the arity of the target."
throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION,
this.name, spelParamCount, (declaredParamCount - 1) + " or more");
}
}
else if (spelParamCount != declaredParamCount) {
// Incorrect number and not varargs. Perhaps a subset of arguments was provided,
// but the MethodHandle wasn't bound?
throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION,
this.name, functionArgs.length, declaredParamCount);
this.name, spelParamCount, declaredParamCount);
}

// simplest case: the MethodHandle is fully bound or represents a static method with no params:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,12 @@ private static void populateMethodHandles(StandardEvaluationContext testContext)
MethodHandle formatObjectVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
}

// #add(int, int)
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"add", MethodType.methodType(int.class, int.class, int.class));
testContext.registerFunction("add", add);
}

/**
* Register some variables that can be referenced from the tests
Expand Down Expand Up @@ -163,4 +168,8 @@ public static String formatObjectVarargs(String format, Object... args) {
return String.format(format, args);
}

public static int add(int x, int y) {
return x + y;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ void functionInvocationWithIncorrectNumberOfArguments() {
evaluateAndCheckError("#reverseInt(1,2)", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "reverseInt", 2, 3);
evaluateAndCheckError("#reverseInt(1,2,3,4)", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "reverseInt", 4, 3);

// MethodHandle: #message(template, args...)
evaluateAndCheckError("#message()", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "message", 0, 2);
evaluateAndCheckError("#message('%s')", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "message", 1, 2);
// MethodHandle: #message(String, Object...)
evaluateAndCheckError("#message()", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "message", 0, "1 or more");

// MethodHandle: #add(int, int)
evaluateAndCheckError("#add()", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "add", 0, 2);
evaluateAndCheckError("#add(1)", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "add", 1, 2);
evaluateAndCheckError("#add(1, 2, 3)", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "add", 3, 2);
}

@Test
Expand Down Expand Up @@ -107,12 +111,6 @@ void functionWithVarargs() {
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);
Expand All @@ -128,13 +126,21 @@ void functionWithVarargsViaMethodHandle_CurrentlyFailing() {
void functionWithVarargsViaMethodHandle() {
// 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("#add(3, 4)", 7, Integer.class);
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("#add('2', 5.0)", 7, Integer.class);
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);

Expand Down

0 comments on commit a0f5c16

Please sign in to comment.