Allow MethodReference to define a more flexible signature
This commit moves MethodReference to an interface with a default implementation that relies on a MethodSpec. Such an arrangement avoid the need of specifying attributes of the method such as whether it is static or not. The resolution of the invocation block now takes an ArgumentCodeGenerator rather than the raw arguments. Doing so gives the opportunity to create more flexible signatures. See gh-29005
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aot.generate;
|
||||
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.CodeBlock;
|
||||
import org.springframework.javapoet.MethodSpec;
|
||||
import org.springframework.javapoet.MethodSpec.Builder;
|
||||
import org.springframework.javapoet.TypeName;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultMethodReference}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class DefaultMethodReferenceTests {
|
||||
|
||||
private static final String EXPECTED_STATIC = "org.springframework.aot.generate.DefaultMethodReferenceTests::someMethod";
|
||||
|
||||
private static final String EXPECTED_ANONYMOUS_INSTANCE = "<instance>::someMethod";
|
||||
|
||||
private static final String EXPECTED_DECLARED_INSTANCE = "<org.springframework.aot.generate.DefaultMethodReferenceTests>::someMethod";
|
||||
|
||||
private static final ClassName TEST_CLASS_NAME = ClassName.get("com.example", "Test");
|
||||
|
||||
private static final ClassName INITIALIZER_CLASS_NAME = ClassName.get("com.example", "Initializer");
|
||||
|
||||
@Test
|
||||
void createWithStringCreatesMethodReference() {
|
||||
MethodSpec method = createTestMethod("someMethod", new TypeName[0]);
|
||||
MethodReference reference = new DefaultMethodReference(method, null);
|
||||
assertThat(reference).hasToString(EXPECTED_ANONYMOUS_INSTANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithClassNameAndStringCreateMethodReference() {
|
||||
ClassName declaringClass = ClassName.get(DefaultMethodReferenceTests.class);
|
||||
MethodReference reference = createMethodReference("someMethod", new TypeName[0], declaringClass);
|
||||
assertThat(reference).hasToString(EXPECTED_DECLARED_INSTANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithStaticAndClassAndStringCreatesMethodReference() {
|
||||
ClassName declaringClass = ClassName.get(DefaultMethodReferenceTests.class);
|
||||
MethodReference reference = createStaticMethodReference("someMethod", declaringClass);
|
||||
assertThat(reference).hasToString(EXPECTED_STATIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCodeBlock() {
|
||||
assertThat(createLocalMethodReference("methodName").toCodeBlock())
|
||||
.isEqualTo(CodeBlock.of("this::methodName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCodeBlockWithStaticMethod() {
|
||||
assertThat(createStaticMethodReference("methodName", TEST_CLASS_NAME).toCodeBlock())
|
||||
.isEqualTo(CodeBlock.of("com.example.Test::methodName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCodeBlockWithStaticMethodRequiresDeclaringClass() {
|
||||
MethodSpec method = createTestMethod("methodName", new TypeName[0], Modifier.STATIC);
|
||||
MethodReference methodReference = new DefaultMethodReference(method, null);
|
||||
assertThatIllegalArgumentException().isThrownBy(methodReference::toCodeBlock)
|
||||
.withMessage("static method reference must define a declaring class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithNullDeclaringClassAndTargetClass() {
|
||||
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
|
||||
MethodReference methodReference = new DefaultMethodReference(method, null);
|
||||
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), TEST_CLASS_NAME))
|
||||
.isEqualTo(CodeBlock.of("methodName()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithNullDeclaringClassAndNullTargetClass() {
|
||||
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
|
||||
MethodReference methodReference = new DefaultMethodReference(method, null);
|
||||
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none()))
|
||||
.isEqualTo(CodeBlock.of("methodName()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithDeclaringClassAndNullTargetClass() {
|
||||
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
|
||||
MethodReference methodReference = new DefaultMethodReference(method, TEST_CLASS_NAME);
|
||||
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none()))
|
||||
.isEqualTo(CodeBlock.of("new com.example.Test().methodName()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithMatchingTargetClass() {
|
||||
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
|
||||
MethodReference methodReference = new DefaultMethodReference(method, TEST_CLASS_NAME);
|
||||
CodeBlock invocation = methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), TEST_CLASS_NAME);
|
||||
// Assume com.example.Test is in a `test` variable.
|
||||
assertThat(CodeBlock.of("$L.$L", "test", invocation)).isEqualTo(CodeBlock.of("test.methodName()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithNonMatchingDeclaringClass() {
|
||||
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
|
||||
MethodReference methodReference = new DefaultMethodReference(method, TEST_CLASS_NAME);
|
||||
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), INITIALIZER_CLASS_NAME))
|
||||
.isEqualTo(CodeBlock.of("new com.example.Test().methodName()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithMatchingArg() {
|
||||
MethodReference methodReference = createLocalMethodReference("methodName", ClassName.get(String.class));
|
||||
ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator.of(String.class, "stringArg");
|
||||
assertThat(methodReference.toInvokeCodeBlock(argCodeGenerator))
|
||||
.isEqualTo(CodeBlock.of("methodName(stringArg)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithMatchingArgs() {
|
||||
MethodReference methodReference = createLocalMethodReference("methodName",
|
||||
ClassName.get(Integer.class), ClassName.get(String.class));
|
||||
ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator.of(String.class, "stringArg")
|
||||
.and(Integer.class, "integerArg");
|
||||
assertThat(methodReference.toInvokeCodeBlock(argCodeGenerator))
|
||||
.isEqualTo(CodeBlock.of("methodName(integerArg, stringArg)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithNonMatchingArg() {
|
||||
MethodReference methodReference = createLocalMethodReference("methodName",
|
||||
ClassName.get(Integer.class), ClassName.get(String.class));
|
||||
ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator.of(Integer.class, "integerArg");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> methodReference.toInvokeCodeBlock(argCodeGenerator))
|
||||
.withMessageContaining("parameter 1 of type java.lang.String is not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithStaticMethodAndMatchingDeclaringClass() {
|
||||
MethodReference methodReference = createStaticMethodReference("methodName", TEST_CLASS_NAME);
|
||||
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), TEST_CLASS_NAME))
|
||||
.isEqualTo(CodeBlock.of("methodName()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWithStaticMethodAndSeparateDeclaringClass() {
|
||||
MethodReference methodReference = createStaticMethodReference("methodName", TEST_CLASS_NAME);
|
||||
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), INITIALIZER_CLASS_NAME))
|
||||
.isEqualTo(CodeBlock.of("com.example.Test.methodName()"));
|
||||
}
|
||||
|
||||
|
||||
private MethodReference createLocalMethodReference(String name, TypeName... argumentTypes) {
|
||||
return createMethodReference(name, argumentTypes, null);
|
||||
}
|
||||
|
||||
private MethodReference createMethodReference(String name, TypeName[] argumentTypes, @Nullable ClassName declaringClass) {
|
||||
MethodSpec method = createTestMethod(name, argumentTypes);
|
||||
return new DefaultMethodReference(method, declaringClass);
|
||||
}
|
||||
|
||||
private MethodReference createStaticMethodReference(String name, ClassName declaringClass, TypeName... argumentTypes) {
|
||||
MethodSpec method = createTestMethod(name, argumentTypes, Modifier.STATIC);
|
||||
return new DefaultMethodReference(method, declaringClass);
|
||||
}
|
||||
|
||||
private MethodSpec createTestMethod(String name, TypeName[] argumentTypes, Modifier... modifiers) {
|
||||
Builder method = MethodSpec.methodBuilder(name);
|
||||
for (int i = 0; i < argumentTypes.length; i++) {
|
||||
method.addParameter(argumentTypes[i], "args" + i);
|
||||
}
|
||||
method.addModifiers(modifiers);
|
||||
return method.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import javax.lang.model.element.Modifier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.CodeBlock;
|
||||
import org.springframework.javapoet.MethodSpec;
|
||||
@@ -67,8 +68,8 @@ class GeneratedMethodTests {
|
||||
GeneratedMethod generatedMethod = create(emptyMethod);
|
||||
MethodReference methodReference = generatedMethod.toMethodReference();
|
||||
assertThat(methodReference).isNotNull();
|
||||
assertThat(methodReference.toInvokeCodeBlock("test"))
|
||||
.isEqualTo(CodeBlock.of("test.spring()"));
|
||||
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), TEST_CLASS_NAME))
|
||||
.isEqualTo(CodeBlock.of("spring()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -76,7 +77,8 @@ class GeneratedMethodTests {
|
||||
GeneratedMethod generatedMethod = create(method -> method.addModifiers(Modifier.STATIC));
|
||||
MethodReference methodReference = generatedMethod.toMethodReference();
|
||||
assertThat(methodReference).isNotNull();
|
||||
assertThat(methodReference.toInvokeCodeBlock())
|
||||
ClassName anotherDeclaringClass = ClassName.get("com.example", "Another");
|
||||
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), anotherDeclaringClass))
|
||||
.isEqualTo(CodeBlock.of("com.example.Test.spring()"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aot.generate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.javapoet.ClassName;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link MethodReference}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class MethodReferenceTests {
|
||||
|
||||
private static final String EXPECTED_STATIC = "org.springframework.aot.generate.MethodReferenceTests::someMethod";
|
||||
|
||||
private static final String EXPECTED_ANONYMOUS_INSTANCE = "<instance>::someMethod";
|
||||
|
||||
private static final String EXPECTED_DECLARED_INSTANCE = "<org.springframework.aot.generate.MethodReferenceTests>::someMethod";
|
||||
|
||||
|
||||
@Test
|
||||
void ofWithStringWhenMethodNameIsNullThrowsException() {
|
||||
String methodName = null;
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.of(methodName))
|
||||
.withMessage("'methodName' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithStringCreatesMethodReference() {
|
||||
String methodName = "someMethod";
|
||||
MethodReference reference = MethodReference.of(methodName);
|
||||
assertThat(reference).hasToString(EXPECTED_ANONYMOUS_INSTANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithClassAndStringWhenDeclaringClassIsNullThrowsException() {
|
||||
Class<?> declaringClass = null;
|
||||
String methodName = "someMethod";
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.of(declaringClass, methodName))
|
||||
.withMessage("'declaringClass' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithClassAndStringWhenMethodNameIsNullThrowsException() {
|
||||
Class<?> declaringClass = MethodReferenceTests.class;
|
||||
String methodName = null;
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.of(declaringClass, methodName))
|
||||
.withMessage("'methodName' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithClassAndStringCreatesMethodReference() {
|
||||
Class<?> declaringClass = MethodReferenceTests.class;
|
||||
String methodName = "someMethod";
|
||||
MethodReference reference = MethodReference.of(declaringClass, methodName);
|
||||
assertThat(reference).hasToString(EXPECTED_DECLARED_INSTANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithClassNameAndStringWhenDeclaringClassIsNullThrowsException() {
|
||||
ClassName declaringClass = null;
|
||||
String methodName = "someMethod";
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.of(declaringClass, methodName))
|
||||
.withMessage("'declaringClass' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithClassNameAndStringWhenMethodNameIsNullThrowsException() {
|
||||
ClassName declaringClass = ClassName.get(MethodReferenceTests.class);
|
||||
String methodName = null;
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.of(declaringClass, methodName))
|
||||
.withMessage("'methodName' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithClassNameAndStringCreateMethodReference() {
|
||||
ClassName declaringClass = ClassName.get(MethodReferenceTests.class);
|
||||
String methodName = "someMethod";
|
||||
MethodReference reference = MethodReference.of(declaringClass, methodName);
|
||||
assertThat(reference).hasToString(EXPECTED_DECLARED_INSTANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofStaticWithClassAndStringWhenDeclaringClassIsNullThrowsException() {
|
||||
Class<?> declaringClass = null;
|
||||
String methodName = "someMethod";
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName))
|
||||
.withMessage("'declaringClass' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofStaticWithClassAndStringWhenMethodNameIsEmptyThrowsException() {
|
||||
Class<?> declaringClass = MethodReferenceTests.class;
|
||||
String methodName = null;
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName))
|
||||
.withMessage("'methodName' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofStaticWithClassAndStringCreatesMethodReference() {
|
||||
Class<?> declaringClass = MethodReferenceTests.class;
|
||||
String methodName = "someMethod";
|
||||
MethodReference reference = MethodReference.ofStatic(declaringClass, methodName);
|
||||
assertThat(reference).hasToString(EXPECTED_STATIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofStaticWithClassNameAndGeneratedMethodNameWhenDeclaringClassIsNullThrowsException() {
|
||||
ClassName declaringClass = null;
|
||||
String methodName = "someMethod";
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName))
|
||||
.withMessage("'declaringClass' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofStaticWithClassNameAndGeneratedMethodNameWhenMethodNameIsEmptyThrowsException() {
|
||||
ClassName declaringClass = ClassName.get(MethodReferenceTests.class);
|
||||
String methodName = null;
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName))
|
||||
.withMessage("'methodName' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofStaticWithClassNameAndGeneratedMethodNameCreatesMethodReference() {
|
||||
ClassName declaringClass = ClassName.get(MethodReferenceTests.class);
|
||||
String methodName = "someMethod";
|
||||
MethodReference reference = MethodReference.ofStatic(declaringClass, methodName);
|
||||
assertThat(reference).hasToString(EXPECTED_STATIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNull() {
|
||||
MethodReference reference = MethodReference.of("someMethod");
|
||||
assertThat(reference.toCodeBlock(null)).hasToString("this::someMethod");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNotNull() {
|
||||
MethodReference reference = MethodReference.of("someMethod");
|
||||
assertThat(reference.toCodeBlock("myInstance"))
|
||||
.hasToString("myInstance::someMethod");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNull() {
|
||||
MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class,
|
||||
"someMethod");
|
||||
assertThat(reference.toCodeBlock(null)).hasToString(EXPECTED_STATIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNotNullThrowsException() {
|
||||
MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class,
|
||||
"someMethod");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> reference.toCodeBlock("myInstance")).withMessage(
|
||||
"'instanceVariable' must be null for static method references");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNull() {
|
||||
MethodReference reference = MethodReference.of("someMethod");
|
||||
assertThat(reference.toInvokeCodeBlock()).hasToString("someMethod()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNullAndHasDecalredClass() {
|
||||
MethodReference reference = MethodReference.of(MethodReferenceTests.class,
|
||||
"someMethod");
|
||||
assertThat(reference.toInvokeCodeBlock()).hasToString(
|
||||
"new org.springframework.aot.generate.MethodReferenceTests().someMethod()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNotNull() {
|
||||
MethodReference reference = MethodReference.of("someMethod");
|
||||
assertThat(reference.toInvokeCodeBlock("myInstance"))
|
||||
.hasToString("myInstance.someMethod()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNull() {
|
||||
MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class,
|
||||
"someMethod");
|
||||
assertThat(reference.toInvokeCodeBlock()).hasToString(
|
||||
"org.springframework.aot.generate.MethodReferenceTests.someMethod()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInvokeCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNotNullThrowsException() {
|
||||
MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class,
|
||||
"someMethod");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> reference.toInvokeCodeBlock("myInstance")).withMessage(
|
||||
"'instanceVariable' must be null for static method references");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user