From 6e66eaadec18fe38104aec8e7024bbf8bb3b5374 Mon Sep 17 00:00:00 2001 From: Axel RICHARD Date: Tue, 14 Apr 2026 17:21:30 +0200 Subject: [PATCH] [enh] Support overloaded ServiceMethod references Add overload-safe ServiceMethod factories that accept explicit Java signature types so ambiguous service method references can be resolved. Also store the resolved Method declaration instead of only the method name, add focused tests for overloaded services, and update the developer documentation with the new API usage. Signed-off-by: Axel RICHARD --- .../org/eclipse/syson/util/ServiceMethod.java | 237 ++++++++++++++-- .../eclipse/syson/util/ServiceMethodTest.java | 256 +++++++++++++++++- .../syson/tests/GeneralPurposeTests.java | 6 +- .../modules/developer-guide/pages/index.adoc | 4 + 4 files changed, 473 insertions(+), 30 deletions(-) diff --git a/backend/services/syson-services/src/main/java/org/eclipse/syson/util/ServiceMethod.java b/backend/services/syson-services/src/main/java/org/eclipse/syson/util/ServiceMethod.java index e3acac8cc..6c6f57fa5 100644 --- a/backend/services/syson-services/src/main/java/org/eclipse/syson/util/ServiceMethod.java +++ b/backend/services/syson-services/src/main/java/org/eclipse/syson/util/ServiceMethod.java @@ -13,6 +13,7 @@ package org.eclipse.syson.util; import java.io.Serializable; +import java.lang.invoke.MethodType; import java.lang.invoke.SerializedLambda; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -72,6 +73,10 @@ * } * *

+ * Overloaded services: if several service methods share the same name, use the factory overloads that also take the + * service class and Java parameter types, for example + * {@code ServiceMethod.of1(EObjectServices.class, EObjectServices::eGet, EObject.class, EStructuralFeature.class)}. + *

* Performance: this uses reflection once per reference at startup to read a method name. The cost is negligible * compared to normal init work. * @@ -80,12 +85,15 @@ */ public final class ServiceMethod { + private final Method declaration; + private final String name; private final int arity; - private ServiceMethod(String name, int arity) { - this.name = name; + private ServiceMethod(Method declaration, int arity) { + this.declaration = declaration; + this.name = declaration.getName(); this.arity = arity; } @@ -98,6 +106,15 @@ public String name() { return this.name; } + /** + * the Java declaration that will be called from AQL. + * + * @return the declaration. + */ + public Method declaration() { + return this.declaration; + } + /** * Build {@code aql:self.method(...)} for the captured service name. *

@@ -149,50 +166,173 @@ public String aql(String var, String... params) { * Instance method with signature {@code R method(T self)}. */ public static ServiceMethod of0(Inst0 ref) { - return new ServiceMethod(methodName(ref), 0); + return new ServiceMethod(method(ref), 0); + } + + /** + * Instance method with signature {@code R method(T self)}. + *

+ * Use this overload when the referenced Java service is overloaded and you need to disambiguate on the + * {@code self} type. + */ + public static ServiceMethod of0(Class selfType, Inst0 ref) { + return new ServiceMethod(method(ref, selfType), 0); + } + + /** + * Instance method with signature {@code R method(T self)}. + *

+ * Use this overload when the referenced Java service is overloaded and you need to disambiguate on the declaring + * service and {@code self} types. + */ + public static ServiceMethod of0(Class serviceType, Inst0 ref, Class selfType) { + return new ServiceMethod(method(serviceType, ref, selfType), 0); } /** * Instance method with signature {@code R method(T self, P1 p1)}. */ public static ServiceMethod of1(Inst1 ref) { - return new ServiceMethod(methodName(ref), 1); + return new ServiceMethod(method(ref), 1); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1)}. + *

+ * Use this overload when the referenced Java service is overloaded and you need to disambiguate on parameter + * types. + */ + public static ServiceMethod of1(Class selfType, Class p1Type, Inst1 ref) { + return new ServiceMethod(method(ref, selfType, p1Type), 1); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1)}. + *

+ * Use this overload when the referenced Java service is overloaded and you need to disambiguate on the declaring + * service and parameter types. + */ + public static ServiceMethod of1(Class serviceType, Inst1 ref, Class selfType, Class p1Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type), 1); } /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2)}. */ public static ServiceMethod of2(Inst2 ref) { - return new ServiceMethod(methodName(ref), 2); + return new ServiceMethod(method(ref), 2); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2)}. + */ + public static ServiceMethod of2(Class selfType, Class p1Type, Class p2Type, Inst2 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type), 2); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2)}. + */ + public static ServiceMethod of2(Class serviceType, Inst2 ref, Class selfType, Class p1Type, Class p2Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type), 2); } /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. */ public static ServiceMethod of3(Inst3 ref) { - return new ServiceMethod(methodName(ref), 3); + return new ServiceMethod(method(ref), 3); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. + */ + public static ServiceMethod of3(Class selfType, Class p1Type, Class p2Type, Class p3Type, Inst3 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type), 3); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. + */ + public static ServiceMethod of3(Class serviceType, Inst3 ref, Class selfType, Class p1Type, Class p2Type, + Class p3Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type, p3Type), 3); } /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4)}. */ public static ServiceMethod of4(Inst4 ref) { - return new ServiceMethod(methodName(ref), 4); + return new ServiceMethod(method(ref), 4); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4)}. + */ + public static ServiceMethod of4(Class selfType, Class p1Type, Class p2Type, Class p3Type, Class p4Type, + Inst4 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type, p4Type), 4); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4)}. + */ + public static ServiceMethod of4(Class serviceType, Inst4 ref, Class selfType, Class p1Type, + Class p2Type, Class p3Type, Class p4Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type, p3Type, p4Type), 4); } /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5)}. */ public static ServiceMethod of5(Inst5 ref) { - return new ServiceMethod(methodName(ref), 5); + return new ServiceMethod(method(ref), 5); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5)}. + */ + public static ServiceMethod of5(Class selfType, Class p1Type, Class p2Type, Class p3Type, Class p4Type, + Class p5Type, Inst5 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type, p4Type, p5Type), 5); } + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5)}. + */ + // CHECKSTYLE:OFF + public static ServiceMethod of5(Class serviceType, Inst5 ref, Class selfType, Class p1Type, + Class p2Type, Class p3Type, Class p4Type, Class p5Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type, p3Type, p4Type, p5Type), 5); + } + // CHECKSTYLE:ON + /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6)}. */ public static ServiceMethod of6(Inst6 ref) { - return new ServiceMethod(methodName(ref), 6); + return new ServiceMethod(method(ref), 6); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6)}. + */ + // CHECKSTYLE:OFF + public static ServiceMethod of6(Class selfType, Class p1Type, Class p2Type, Class p3Type, Class p4Type, + Class p5Type, Class p6Type, Inst6 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type, p4Type, p5Type, p6Type), 6); } + // CHECKSTYLE:ON + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6)}. + */ + // CHECKSTYLE:OFF + public static ServiceMethod of6(Class serviceType, Inst6 ref, Class selfType, + Class p1Type, Class p2Type, Class p3Type, Class p4Type, Class p5Type, Class p6Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type, p3Type, p4Type, p5Type, p6Type), 6); + } + // CHECKSTYLE:ON // ---------------------- Factories for static methods ---------------------- @@ -200,28 +340,56 @@ public static ServiceMethod of6(Inst6 ServiceMethod ofStatic0(IStat0 ref) { - return new ServiceMethod(methodName(ref), 0); + return new ServiceMethod(method(ref), 0); + } + + /** + * Static method with signature {@code R method(T self)}. + */ + public static ServiceMethod ofStatic0(Class selfType, IStat0 ref) { + return new ServiceMethod(method(ref, selfType), 0); } /** * Static method with signature {@code R method(T self, P1 p1)}. */ public static ServiceMethod ofStatic1(IStat1 ref) { - return new ServiceMethod(methodName(ref), 1); + return new ServiceMethod(method(ref), 1); + } + + /** + * Static method with signature {@code R method(T self, P1 p1)}. + */ + public static ServiceMethod ofStatic1(Class selfType, Class p1Type, IStat1 ref) { + return new ServiceMethod(method(ref, selfType, p1Type), 1); } /** * Static method with signature {@code R method(T self, P1 p1, P2 p2)}. */ public static ServiceMethod ofStatic2(IStat2 ref) { - return new ServiceMethod(methodName(ref), 2); + return new ServiceMethod(method(ref), 2); + } + + /** + * Static method with signature {@code R method(T self, P1 p1, P2 p2)}. + */ + public static ServiceMethod ofStatic2(Class selfType, Class p1Type, Class p2Type, IStat2 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type), 2); } /** * Static method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. */ public static ServiceMethod ofStatic3(IStat3 ref) { - return new ServiceMethod(methodName(ref), 3); + return new ServiceMethod(method(ref), 3); + } + + /** + * Static method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. + */ + public static ServiceMethod ofStatic3(Class selfType, Class p1Type, Class p2Type, Class p3Type, IStat3 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type), 3); } // ---------------------- SAMs for method references ---------------------- @@ -427,14 +595,42 @@ public interface IStat3 extends Serializable { // ---------------------- Lambda -> method name ---------------------- - private static String methodName(Serializable lambdaRef) { + private static Method method(Serializable lambdaRef, Class... expectedParameterTypes) { + try { + SerializedLambda lambda = serializedLambda(lambdaRef); + Class implementationClass = Class.forName(lambda.getImplClass().replace('/', '.'), false, lambdaRef.getClass().getClassLoader()); + MethodType methodType = MethodType.fromMethodDescriptorString(lambda.getImplMethodSignature(), implementationClass.getClassLoader()); + Method method = thisClassMethod(implementationClass, lambda.getImplMethodName(), methodType.parameterArray()); + if (expectedParameterTypes.length > 0 && !Arrays.equals(method.getParameterTypes(), expectedParameterTypes)) { + throw new IllegalArgumentException( + MessageFormat.format("Resolved method {0} has parameters {1} but expected {2}", method, Arrays.toString(method.getParameterTypes()), Arrays.toString(expectedParameterTypes))); + } + return method; + } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException | SecurityException | IllegalAccessException e) { + throw new IllegalStateException("Cannot resolve method declaration from lambda", e); + } + } + + private static Method method(Class expectedServiceType, Serializable lambdaRef, Class... expectedParameterTypes) { + Method method = method(lambdaRef, expectedParameterTypes); + if (!expectedServiceType.isAssignableFrom(method.getDeclaringClass())) { + throw new IllegalArgumentException(MessageFormat.format("Resolved method {0} is declared on {1} but expected a service assignable to {2}", method, + method.getDeclaringClass().getName(), expectedServiceType.getName())); + } + return method; + } + + private static SerializedLambda serializedLambda(Serializable lambdaRef) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + Method writeReplace = lambdaRef.getClass().getDeclaredMethod("writeReplace"); + writeReplace.setAccessible(true); + return (SerializedLambda) writeReplace.invoke(lambdaRef); + } + + private static Method thisClassMethod(Class implementationClass, String methodName, Class[] parameterTypes) throws NoSuchMethodException { try { - Method m = lambdaRef.getClass().getDeclaredMethod("writeReplace"); - m.setAccessible(true); - SerializedLambda sl = (SerializedLambda) m.invoke(lambdaRef); - return sl.getImplMethodName(); - } catch (InvocationTargetException | NoSuchMethodException | SecurityException | IllegalAccessException e) { - throw new IllegalStateException("Cannot resolve method name from lambda", e); + return implementationClass.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException exception) { + return implementationClass.getMethod(methodName, parameterTypes); } } @@ -457,4 +653,3 @@ private void checkArity(String... params) { } } } - diff --git a/backend/services/syson-services/src/test/java/org/eclipse/syson/util/ServiceMethodTest.java b/backend/services/syson-services/src/test/java/org/eclipse/syson/util/ServiceMethodTest.java index c6ad72f78..64e384b24 100644 --- a/backend/services/syson-services/src/test/java/org/eclipse/syson/util/ServiceMethodTest.java +++ b/backend/services/syson-services/src/test/java/org/eclipse/syson/util/ServiceMethodTest.java @@ -15,6 +15,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.lang.reflect.Method; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,23 +27,261 @@ */ public class ServiceMethodTest { + private static final String AQL_VALUE = "'value'"; + + private static final String AQL_INT = "2"; + @Test @DisplayName("GIVEN a service method with arity 0, WHEN an AQL expression is constructed with 0 parameter, THEN the expression is returned") public void givenServiceMethodWithArity0WhenAQLExpressionIsConstructedWith0ParameterThenExpressionIsReturned() { - String expression = ServiceMethod.of0(ServiceMethodTest::serviceWithArity0).aqlSelf(); - assertThat(expression).isEqualTo("aql:self.serviceWithArity0()"); - expression = ServiceMethod.of0(ServiceMethodTest::serviceWithArity0).aql("var"); - assertThat(expression).isEqualTo("aql:var.serviceWithArity0()"); + String expression = ServiceMethod.of0(ArityFixture::instance0).aqlSelf(); + assertThat(expression).isEqualTo("aql:self.instance0()"); + expression = ServiceMethod.of0(ArityFixture::instance0).aql("var"); + assertThat(expression).isEqualTo("aql:var.instance0()"); } @Test @DisplayName("GIVEN a service method with arity 0, WHEN an AQL expression is constructed with 1 parameter, THEN an exception is thrown") public void givenServiceMethodWithArity0WhenAQLExpressionIsConstructedWith1ParameterThenExceptionIsThrown() { - assertThatThrownBy(() -> ServiceMethod.of0(ServiceMethodTest::serviceWithArity0).aqlSelf("param1")).isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> ServiceMethod.of0(ServiceMethodTest::serviceWithArity0).aql("var", "param1")).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> ServiceMethod.of0(ArityFixture::instance0).aqlSelf("param1")).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> ServiceMethod.of0(ArityFixture::instance0).aql("var", "param1")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("GIVEN instance service factories, WHEN each arity is used, THEN the expected expressions are returned") + public void givenInstanceServiceFactoriesWhenEachArityIsUsedThenTheExpectedExpressionsAreReturned() throws NoSuchMethodException { + assertThat(ServiceMethod.of0(ArityFixture::instance0).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance0", Object.class)); + assertThat(ServiceMethod.of1(ArityFixture::instance1).aql("ctx", AQL_VALUE)).isEqualTo("aql:ctx.instance1('value')"); + assertThat(ServiceMethod.of2(ArityFixture::instance2).aqlSelf(AQL_VALUE, AQL_INT)).isEqualTo("aql:self.instance2('value',2)"); + assertThat(ServiceMethod.of3(ArityFixture::instance3).aqlSelf(AQL_VALUE, AQL_INT, "true")).isEqualTo("aql:self.instance3('value',2,true)"); + assertThat(ServiceMethod.of4(ArityFixture::instance4).aqlSelf(AQL_VALUE, AQL_INT, "true", "4.0")) + .isEqualTo("aql:self.instance4('value',2,true,4.0)"); + assertThat(ServiceMethod.of5(ArityFixture::instance5).aqlSelf(AQL_VALUE, AQL_INT, "true", "4.0", "5L")) + .isEqualTo("aql:self.instance5('value',2,true,4.0,5L)"); + assertThat(ServiceMethod.of6(ArityFixture::instance6).aqlSelf(AQL_VALUE, AQL_INT, "true", "4.0", "5L", "6.0f")) + .isEqualTo("aql:self.instance6('value',2,true,4.0,5L,6.0f)"); + } + + @Test + @DisplayName("GIVEN typed instance service factories, WHEN each overload is used, THEN the expected declarations are resolved") + public void givenTypedInstanceServiceFactoriesWhenEachOverloadIsUsedThenTheExpectedDeclarationsAreResolved() throws NoSuchMethodException { + assertThat(ServiceMethod.of0(Object.class, ArityFixture::instance0).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance0", Object.class)); + assertThat(ServiceMethod.of0(ArityFixture.class, ArityFixture::instance0, Object.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance0", Object.class)); + assertThat(ServiceMethod.of1(Object.class, String.class, ArityFixture::instance1).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance1", Object.class, String.class)); + assertThat(ServiceMethod.of1(ArityFixture.class, ArityFixture::instance1, Object.class, String.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance1", Object.class, String.class)); + assertThat(ServiceMethod.of2(Object.class, String.class, Integer.class, ArityFixture::instance2).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance2", Object.class, String.class, Integer.class)); + assertThat(ServiceMethod.of2(ArityFixture.class, ArityFixture::instance2, Object.class, String.class, Integer.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance2", Object.class, String.class, Integer.class)); + assertThat(ServiceMethod.of3(Object.class, String.class, Integer.class, Boolean.class, ArityFixture::instance3).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance3", Object.class, String.class, Integer.class, Boolean.class)); + assertThat(ServiceMethod.of3(ArityFixture.class, ArityFixture::instance3, Object.class, String.class, Integer.class, Boolean.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance3", Object.class, String.class, Integer.class, Boolean.class)); + assertThat(ServiceMethod.of4(Object.class, String.class, Integer.class, Boolean.class, Double.class, ArityFixture::instance4).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance4", Object.class, String.class, Integer.class, Boolean.class, Double.class)); + assertThat(ServiceMethod.of4(ArityFixture.class, ArityFixture::instance4, Object.class, String.class, Integer.class, Boolean.class, Double.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance4", Object.class, String.class, Integer.class, Boolean.class, Double.class)); + assertThat(ServiceMethod.of5(Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, ArityFixture::instance5).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance5", Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class)); + assertThat(ServiceMethod.of5(ArityFixture.class, ArityFixture::instance5, Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class) + .declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance5", Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class)); + assertThat(ServiceMethod.of6(Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class, ArityFixture::instance6) + .declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance6", Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class)); + assertThat(ServiceMethod + .of6(ArityFixture.class, ArityFixture::instance6, Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class) + .declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance6", Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class)); + } + + @Test + @DisplayName("GIVEN static service factories, WHEN each arity is used, THEN the expected expressions are returned") + public void givenStaticServiceFactoriesWhenEachArityIsUsedThenTheExpectedExpressionsAreReturned() throws NoSuchMethodException { + assertThat(ServiceMethod.ofStatic0(StaticFixture::static0).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static0", Object.class)); + assertThat(ServiceMethod.ofStatic0(Object.class, StaticFixture::static0).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static0", Object.class)); + assertThat(ServiceMethod.ofStatic1(StaticFixture::static1).aqlSelf(AQL_VALUE)).isEqualTo("aql:self.static1('value')"); + assertThat(ServiceMethod.ofStatic1(Object.class, String.class, StaticFixture::static1).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static1", Object.class, String.class)); + assertThat(ServiceMethod.ofStatic2(StaticFixture::static2).aql("ctx", AQL_VALUE, AQL_INT)).isEqualTo("aql:ctx.static2('value',2)"); + assertThat(ServiceMethod.ofStatic2(Object.class, String.class, Integer.class, StaticFixture::static2).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static2", Object.class, String.class, Integer.class)); + assertThat(ServiceMethod.ofStatic3(StaticFixture::static3).aqlSelf(AQL_VALUE, AQL_INT, "true")).isEqualTo("aql:self.static3('value',2,true)"); + assertThat(ServiceMethod.ofStatic3(Object.class, String.class, Integer.class, Boolean.class, StaticFixture::static3).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static3", Object.class, String.class, Integer.class, Boolean.class)); + } + + @Test + @DisplayName("GIVEN a service method with another arity, WHEN the wrong number of parameters is provided, THEN an exception is thrown") + public void givenServiceMethodWithAnotherArityWhenWrongNumberOfParametersIsProvidedThenExceptionIsThrown() { + assertThatThrownBy(() -> ServiceMethod.of3(ArityFixture::instance3).aqlSelf(AQL_VALUE, AQL_INT)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("instance3") + .hasMessageContaining("arity of 3"); + } + + @Test + @DisplayName("GIVEN an invalid AQL variable, WHEN aql is called, THEN an exception is thrown") + public void givenInvalidAQLVariableWhenAqlIsCalledThenExceptionIsThrown() { + assertThatThrownBy(() -> ServiceMethod.of0(ArityFixture::instance0).aql((String) null)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> ServiceMethod.of0(ArityFixture::instance0).aql("")).isInstanceOf(IllegalArgumentException.class); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + @DisplayName("GIVEN wrong expected parameter types, WHEN the declaration is resolved, THEN an exception is thrown") + public void givenWrongExpectedParameterTypesWhenDeclarationIsResolvedThenAnExceptionIsThrown() { + ServiceMethod.Inst1 ref = ArityFixture::instance1; + + assertThatThrownBy(() -> ServiceMethod.of1(Object.class, Integer.class, (ServiceMethod.Inst1) ref)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("expected"); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + @DisplayName("GIVEN a wrong expected service type, WHEN the declaration is resolved, THEN an exception is thrown") + public void givenWrongExpectedServiceTypeWhenDeclarationIsResolvedThenAnExceptionIsThrown() { + ServiceMethod.Inst1 ref = ArityFixture::instance1; + + assertThatThrownBy(() -> ServiceMethod.of1((Class) WrongService.class, (ServiceMethod.Inst1) ref, Object.class, String.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("expected a service assignable"); + } + + @Test + @DisplayName("GIVEN overloaded services, WHEN explicit signature types are provided, THEN the expected declaration is resolved") + public void givenOverloadedServicesWhenExplicitSignatureTypesAreProvidedThenTheExpectedDeclarationIsResolved() throws NoSuchMethodException { + ServiceMethod serviceMethod = ServiceMethod.of1(OverloadedService.class, OverloadedService::overloaded, Object.class, Integer.class); + + Method expectedDeclaration = OverloadedService.class.getDeclaredMethod("overloaded", Object.class, Integer.class); + assertThat(serviceMethod.name()).isEqualTo("overloaded"); + assertThat(serviceMethod.declaration()).isEqualTo(expectedDeclaration); + assertThat(serviceMethod.declaration().getReturnType()).isEqualTo(Integer.class); + assertThat(serviceMethod.aqlSelf("42")).isEqualTo("aql:self.overloaded(42)"); } - private Object serviceWithArity0(Object self) { - return null; + @Test + @DisplayName("GIVEN overloaded services, WHEN explicit self type is provided, THEN arity-0 overloads can be selected") + public void givenOverloadedServicesWhenExplicitSelfTypeIsProvidedThenArity0OverloadsCanBeSelected() throws NoSuchMethodException { + ServiceMethod serviceMethod = ServiceMethod.of0(OverloadedService.class, OverloadedService::overloaded, String.class); + + Method expectedDeclaration = OverloadedService.class.getDeclaredMethod("overloaded", String.class); + assertThat(serviceMethod.declaration()).isEqualTo(expectedDeclaration); + assertThat(serviceMethod.declaration().getReturnType()).isEqualTo(String.class); + assertThat(serviceMethod.aqlSelf()).isEqualTo("aql:self.overloaded()"); + } + + @Test + @DisplayName("GIVEN overloaded services, WHEN another explicit self type is provided, THEN the matching arity-0 overload is selected") + public void givenOverloadedServicesWhenAnotherExplicitSelfTypeIsProvidedThenTheMatchingArity0OverloadIsSelected() throws NoSuchMethodException { + ServiceMethod serviceMethod = ServiceMethod.of0(OverloadedService.class, OverloadedService::overloaded, Integer.class); + + Method expectedDeclaration = OverloadedService.class.getDeclaredMethod("overloaded", Integer.class); + assertThat(serviceMethod.declaration()).isEqualTo(expectedDeclaration); + assertThat(serviceMethod.declaration().getReturnType()).isEqualTo(Integer.class); + assertThat(serviceMethod.aqlSelf()).isEqualTo("aql:self.overloaded()"); + } + + @Test + @DisplayName("GIVEN overloaded services, WHEN another explicit parameter type is provided, THEN the matching arity-1 overload is selected") + public void givenOverloadedServicesWhenAnotherExplicitParameterTypeIsProvidedThenTheMatchingArity1OverloadIsSelected() throws NoSuchMethodException { + ServiceMethod serviceMethod = ServiceMethod.of1(OverloadedService.class, OverloadedService::overloaded, Object.class, String.class); + + Method expectedDeclaration = OverloadedService.class.getDeclaredMethod("overloaded", Object.class, String.class); + assertThat(serviceMethod.declaration()).isEqualTo(expectedDeclaration); + assertThat(serviceMethod.declaration().getReturnType()).isEqualTo(String.class); + assertThat(serviceMethod.aqlSelf(AQL_VALUE)).isEqualTo("aql:self.overloaded('value')"); + } + + /** + * Fixture used to validate factory methods across supported instance arities. + */ + private static final class ArityFixture { + private Object instance0(Object self) { + return self; + } + + private Object instance1(Object self, String p1) { + return p1; + } + + private Object instance2(Object self, String p1, Integer p2) { + return p2; + } + + private Object instance3(Object self, String p1, Integer p2, Boolean p3) { + return p3; + } + + private Object instance4(Object self, String p1, Integer p2, Boolean p3, Double p4) { + return p4; + } + + private Object instance5(Object self, String p1, Integer p2, Boolean p3, Double p4, Long p5) { + return p5; + } + + private Object instance6(Object self, String p1, Integer p2, Boolean p3, Double p4, Long p5, Float p6) { + return p6; + } + } + + /** + * Fixture used to validate factory methods for static service references. + */ + private static final class StaticFixture { + private static Object static0(Object self) { + return self; + } + + private static Object static1(Object self, String p1) { + return p1; + } + + private static Object static2(Object self, String p1, Integer p2) { + return p2; + } + + private static Object static3(Object self, String p1, Integer p2, Boolean p3) { + return p3; + } + } + + /** + * Fixture used to validate service type mismatch checks. + */ + private static final class WrongService { + private WrongService() { + // Utility fixture. + } + } + + /** + * Fixture used to validate overloaded method resolution. + */ + private static final class OverloadedService { + private String overloaded(String self) { + return self; + } + + private Integer overloaded(Integer self) { + return self; + } + + private String overloaded(Object self, String value) { + return value; + } + + private Integer overloaded(Object self, Integer value) { + return value; + } } } diff --git a/backend/tests/syson-tests/src/test/java/org/eclipse/syson/tests/GeneralPurposeTests.java b/backend/tests/syson-tests/src/test/java/org/eclipse/syson/tests/GeneralPurposeTests.java index 1889cff1a..572455381 100644 --- a/backend/tests/syson-tests/src/test/java/org/eclipse/syson/tests/GeneralPurposeTests.java +++ b/backend/tests/syson-tests/src/test/java/org/eclipse/syson/tests/GeneralPurposeTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024, 2025 Obeo. + * Copyright (c) 2024, 2026 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -67,6 +67,8 @@ public class GeneralPurposeTests { private static final String CHECKSTYLE_INTERFACE_IS_TYPE = "@SuppressWarnings(\"checkstyle:InterfaceIsType\")"; + private static final String CHECKSTYLE_RAWTYPES_UNCHECKED = "@SuppressWarnings({ \"rawtypes\", \"unchecked\" })"; + private static final String NON_NLS = "$NON-NLS-"; private static final String BUILDER = "Builder"; @@ -223,6 +225,8 @@ private void testNoSuppressWarnings(int index, String line, Path javaFilePath, L isValidUsage = true; } else if (line.contains(CHECKSTYLE_INTERFACE_IS_TYPE)) { isValidUsage = true; + } else if (line.contains(CHECKSTYLE_RAWTYPES_UNCHECKED)) { + isValidUsage = true; } if (!isValidUsage) { fail(this.createErrorMessage("@SuppressWarnings", javaFilePath, index)); diff --git a/doc/content/modules/developer-guide/pages/index.adoc b/doc/content/modules/developer-guide/pages/index.adoc index c496a342a..1f4d4f290 100644 --- a/doc/content/modules/developer-guide/pages/index.adoc +++ b/doc/content/modules/developer-guide/pages/index.adoc @@ -549,6 +549,7 @@ When to use which call: Type inference: - if the compiler says something like _The type X does not define methodName(Object, Object,...)_, add a type witness to the factory so it can match the real signature. +- if the service method is overloaded, use the factory overload that also takes the service class and Java parameter types to disambiguate the method reference. Examples: @@ -562,6 +563,9 @@ ServiceMethod. of1(DiagramMutation // predicate isActor(Element) ServiceMethod. of0(ModelQueryAQLService::isActor).aqlSelf(); + +// overloaded EObjectServices::eGet(EObject, EStructuralFeature) +ServiceMethod.of1(EObjectServices.class, EObjectServices::eGet, EObject.class, EStructuralFeature.class).aqlSelf(E_STRUCTURAL_FEATURE); ---- [#generate_openapi]