From 20deed8dbec0a5aed8c9f8a9389cd685b65393be Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Wed, 20 May 2026 19:59:04 +0100 Subject: [PATCH 1/2] Do some tests with refined multi-valued slots. The possibility for a derived class to "refine" a multi-valued slot is more complicated to handle than I thought, due to the way generics work in Java. This commit creates some classes illustrating the case of a refined multi-valued slot, with the currently envisioned way of dealing with such a case. --- .../incenp/linkml/core/playground/Bar.java | 19 +++ .../core/playground/FirstDerivedBar.java | 20 +++ .../core/playground/FirstDerivedFoo.java | 125 ++++++++++++++++ .../incenp/linkml/core/playground/Foo.java | 100 +++++++++++++ .../linkml/core/playground/Playground.java | 115 +++++++++++++++ .../core/playground/SecondDerivedBar.java | 11 ++ .../core/playground/SecondDerivedFoo.java | 13 ++ .../core/playground/ThirdDerivedFoo.java | 139 ++++++++++++++++++ 8 files changed, 542 insertions(+) create mode 100644 core/src/test/java/org/incenp/linkml/core/playground/Bar.java create mode 100644 core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedBar.java create mode 100644 core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedFoo.java create mode 100644 core/src/test/java/org/incenp/linkml/core/playground/Foo.java create mode 100644 core/src/test/java/org/incenp/linkml/core/playground/Playground.java create mode 100644 core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedBar.java create mode 100644 core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedFoo.java create mode 100644 core/src/test/java/org/incenp/linkml/core/playground/ThirdDerivedFoo.java diff --git a/core/src/test/java/org/incenp/linkml/core/playground/Bar.java b/core/src/test/java/org/incenp/linkml/core/playground/Bar.java new file mode 100644 index 0000000..058010c --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/Bar.java @@ -0,0 +1,19 @@ +package org.incenp.linkml.core.playground; + +/** + * An example of a class that is used in a “refined” slot. + *

+ * This class is used in the {@link Foo} class. Some of the classes that are + * derived from Foo uses derived classes instead. + */ +public class Bar { + private String name; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedBar.java b/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedBar.java new file mode 100644 index 0000000..e047920 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedBar.java @@ -0,0 +1,20 @@ +package org.incenp.linkml.core.playground; + +/** + * First derived class from Bar. + *

+ * This class is used, instead of its parent Bar in + * {@link FirstDerivedFoo}. + */ +public class FirstDerivedBar extends Bar { + + private int length; + + public int getLength() { + return length; + } + + public void setLength(int value) { + length = value; + } +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedFoo.java b/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedFoo.java new file mode 100644 index 0000000..628ddea --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedFoo.java @@ -0,0 +1,125 @@ +package org.incenp.linkml.core.playground; + +import java.util.List; + +/** + * An example of a class that refines the range of its slots to make them accept + * only a more specialised subclass. + */ +public class FirstDerivedFoo extends Foo { + + /* + * Overridden read accessor for the `bar` slot. + * + * We override it to ensure that it returns the more specialised subtype. + */ + @Override + public FirstDerivedBar getBar() { + // This cast is perfectly safe because the write accessor below guarantees that + // only a FirstDerivedBar object can be assigned to the slot. + return (FirstDerivedBar) super.getBar(); + } + + /* + * Overridden write accessor for the `bar` slot. + * + * We override it to add a runtime check to enforce the more specialised type + * constraint. We cannot prevent client code from trying to assign an object of + * the wrong type, but if that happens we can at least immediately throw an + * exception. + */ + @Override + public void setBar(Bar value) { + if ( !(value instanceof FirstDerivedBar) ) { + throw new IllegalArgumentException("Invalid bar value"); + } + super.setBar(value); + } + + /* + * Overloaded write accessor for the `bar` slot. + * + * This accessor is not strictly necessary, but it makes it clearer that in this + * class, the value of the `bar` slot should be a `FirstDerivedBar`. It also + * allows to bypass the dynamic check in the normal accessor above, if the + * compiler already knows that the assigned value is a FirstDerivedBar. + */ + public void setBar(FirstDerivedBar value) { + super.setBar(value); + } + + /* + * Overridden “Standard” read accessor. + * + * We override it to ensure it returns the more specialised subtype. + * + * Because the slot could be (and instead is, in this example) refined further + * in subclasses, we must still return a generic wildcard, so this accessor has + * the same limitation as the one it overrides in the `Foo` class: modifying the + * returned list requires an explicit cast into a non-wildcard form. + */ + @Override + @SuppressWarnings("unchecked") + public List getBars() { + // This cast should be safe IFF nobody explicitly modify the value returned by + // this accessor after casting it into a `List`. + return (List) super.getBars(); + } + + /* + * Overridden read accessor with optional creation of the list. + * + * We must override this accessor to ensure that the created list (if the list + * needs to be created) is using the more specialised type. + */ + @Override + public List getBars(boolean create) { + // We can delegate the logic to the parent + return super.getBars(FirstDerivedBar.class, create); + } + + /* + * Overridden parameterised read accessor. + * + * We must override this accessor to add a runtime check that the given type + * parameter is compatible with the more specialised type. + */ + @Override + public List getBars(Class t) { + if ( !FirstDerivedBar.class.isAssignableFrom(t) ) { + throw new IllegalArgumentException("Invalid type parameter"); + } + return super.getBars(t); + } + + /* + * Overridden parameterised read accessor with optional creation of the list. + * + * Same as above: we must override this accessor to add a runtime check on the + * type parameter. + */ + @Override + public List getBars(Class t, boolean create) { + if ( !FirstDerivedBar.class.isAssignableFrom(t) ) { + throw new IllegalArgumentException("Invalid type parameter"); + } + return super.getBars(t, create); + } + + /* + * Overridden “standard” write accessor. + * + * We must override this accessor to include a runtime check. The check must be + * performed on all list items. + */ + @Override + public void setBars(List value) { + for ( Bar b : value ) { + if ( !(b instanceof FirstDerivedBar) ) { + throw new IllegalArgumentException("Invalid bars value"); + } + } + super.setBars(value); + } + +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/Foo.java b/core/src/test/java/org/incenp/linkml/core/playground/Foo.java new file mode 100644 index 0000000..5576616 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/Foo.java @@ -0,0 +1,100 @@ +package org.incenp.linkml.core.playground; + +import java.util.ArrayList; +import java.util.List; + +/** + * An example of a class whose slots are “refined” in derived classes. + */ +public class Foo { + + private Bar bar; + + // We use a wildcard generic to allow derived classes to “refine” the parameter + // type + private List bars; + + /* + * Accessors for the `bar` slot. + * + * Nothing out of the ordinary here. + */ + public Bar getBar() { + return bar; + } + + public void setBar(Bar value) { + bar = value; + } + + /* + * Accessors for the multi-valued `bars` slot. + */ + + /* + * “Standard” read accessor. Its return type is parameterized with a wildcard + * generic to allow derived classes to refine the parameter. + * + * Modifying the returned list is only possible by explicitly casting it to a + * non-wildcard form, as in: + * + * ((List) foo.getBars()).add(new Bar()); + * + * Without the cast, the following would be a compile-time error: + * + * foo.getBars().add(new Bar()); + */ + public List getBars() { + return bars; + } + + /* + * LinkML-Java “Standard” read accessor with optional creation of the list. + * + * This is a convenience accessor, intended to allow client code to dispense + * with a null-ness check. + * + * As for the argument-less read accessor, the return type is a wildcard, so + * modifying the returned list requires an explicit cast. + */ + public List getBars(boolean create) { + if ( bars == null && create ) { + bars = new ArrayList(); + } + return bars; + } + + /* + * Parameterised read accessor. + * + * This is another convenience accessor. This one is intended to allow client + * code to dispense with an explicit cast to modify the list: + * + * foo.getBars(Bar.class).add(new Bar()); + */ + @SuppressWarnings("unchecked") + public List getBars(Class t) { + return (List) bars; + } + + /* + * Parameterised read accessor with optional creation of the list. + * + * This is another convenience accessor, combining the effects of the two + * accessors above. + */ + @SuppressWarnings("unchecked") + public List getBars(Class t, boolean create) { + if ( bars == null && create ) { + bars = new ArrayList(); + } + return (List) bars; + } + + /* + * “Standard” write accessor. + */ + public void setBars(List value) { + bars = value; + } +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/Playground.java b/core/src/test/java/org/incenp/linkml/core/playground/Playground.java new file mode 100644 index 0000000..d137b8d --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/Playground.java @@ -0,0 +1,115 @@ +package org.incenp.linkml.core.playground; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.WildcardType; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class Playground { + + @Test + void testFoo() { + Foo f = new Foo(); + f.getBars(Bar.class, true).add(new Bar()); + // Can add a derived Bar + f.getBars(Bar.class).add(new FirstDerivedBar()); + } + + @Test + void testFirstDerivedFoo() { + FirstDerivedFoo fdf = new FirstDerivedFoo(); + try { + fdf.getBars(Bar.class, true).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + + fdf.getBars(FirstDerivedBar.class, true).add(new FirstDerivedBar()); + // Can access bars items as FirstDerivedBar objects in read mode + fdf.getBars().get(0).setLength(9); + + Foo f = fdf; + try { + // Even when accessed from a Foo object, cannot assign a Bar + f.getBars(Bar.class).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + } + + @Test + void testSecondDerivedFoo() { + SecondDerivedFoo sdf = new SecondDerivedFoo(); + try { + sdf.getBars(Bar.class, true).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + + sdf.getBars(FirstDerivedBar.class, true).add(new FirstDerivedBar()); + // Can access bars items as FirstDerivedBar objects in read mode + sdf.getBars().get(0).setLength(9); + + FirstDerivedFoo fdf = sdf; + try { + // Even when accessed from a FirstDerivedFoo object, cannot assign a Bar + fdf.getBars(Bar.class).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + + Foo f = sdf; + try { + // Even when accessed from a Foo object, cannot assign a Bar + f.getBars(Bar.class).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + } + + @Test + void testThirdDerivedFoo() { + ThirdDerivedFoo tdf = new ThirdDerivedFoo(); + try { + tdf.getBars(FirstDerivedBar.class, true).add(new FirstDerivedBar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + + tdf.getBars(SecondDerivedBar.class, true).add(new SecondDerivedBar()); + // Can access bars items as FirstDerivedBar objects in read mode + tdf.getBars().get(0).setLength(9); + + Foo f = tdf; + try { + // Even when accessed from a Foo object, cannot assign a Bar + f.getBars(Bar.class).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + } + + @Test + void testObtainingParameterBound() { + Class klass = FirstDerivedFoo.class; + Method m = null; + try { + m = klass.getDeclaredMethod("getBars", (Class[]) null); + } catch ( NoSuchMethodException | SecurityException e ) { + Assertions.fail("No getBars method"); + } + + ParameterizedType t = (ParameterizedType) m.getGenericReturnType(); + WildcardType t2 = (WildcardType) t.getActualTypeArguments()[0]; + Assertions.assertEquals(FirstDerivedBar.class, t2.getUpperBounds()[0]); + } +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedBar.java b/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedBar.java new file mode 100644 index 0000000..b13c9a4 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedBar.java @@ -0,0 +1,11 @@ +package org.incenp.linkml.core.playground; + +/** + * Second derived class from Bar. + *

+ * This class is used, instead of its parent FirstDerivedBar, in + * {@link ThirdDerivedFoo}. + */ +public class SecondDerivedBar extends FirstDerivedBar { + +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedFoo.java b/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedFoo.java new file mode 100644 index 0000000..763a144 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedFoo.java @@ -0,0 +1,13 @@ +package org.incenp.linkml.core.playground; + +/** + * An example of a class that inherits from a class that refines its slots, but + * that does no do any refinement itself. + */ +public class SecondDerivedFoo extends FirstDerivedFoo { + + /* + * In this class, the `bar` and `bars` slot are of the same type as in the + * parental `FirstDerivedFoo` class, so no overriding of accessors is necessary. + */ +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/ThirdDerivedFoo.java b/core/src/test/java/org/incenp/linkml/core/playground/ThirdDerivedFoo.java new file mode 100644 index 0000000..9ab295c --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/ThirdDerivedFoo.java @@ -0,0 +1,139 @@ +package org.incenp.linkml.core.playground; + +import java.util.List; + +/** + * An example of a class that refines the range of its slots, that it inherited + * from a class that already refined them. + * + * Importantly, this class has no derived class, so we know its slots cannot be + * further refined by another class. + */ +public class ThirdDerivedFoo extends SecondDerivedFoo { + + /* + * Overridden read accessor for the `bar` slot. + * + * We override it to ensure that it returns the more specialised subtype. + */ + @Override + public SecondDerivedBar getBar() { + return (SecondDerivedBar) super.getBar(); + } + + /* + * Overridden write accessor for the `bar` slot. + * + * We override it to add a runtime check to enforce the more specialised type + * constraint. We cannot prevent client code from trying to assign an object of + * the wrong type, but if that happens we can at least immediately throw an + * exception. + */ + @Override + public void setBar(Bar value) { + if ( !(value instanceof SecondDerivedBar) ) { + throw new IllegalArgumentException("Invalid bar value"); + } + super.setBar(value); + } + + /* + * Second overridden write accessor for the `bar` slot. + * + * Since `FirstDerivedFoo` defined this accessor, we must override it as well, + * otherwise it would allow client code to assign a FirstDerivedBar to the slot. + */ + @Override + public void setBar(FirstDerivedBar value) { + if ( !(value instanceof SecondDerivedBar) ) { + throw new IllegalArgumentException("Invalid bar value"); + } + super.setBar(value); + } + + /* + * Overloaded write accessor for the `bar` slot. + * + * This accessor is not strictly necessary, but it makes it clearer that in this + * class, the value of the `bar` slot should be a `SecondDerivedBar`. It also + * allows to bypass the dynamic check in the normal accessor above, if the + * compiler already knows that the assigned value is a FirstDerivedBar. + */ + public void setBar(SecondDerivedBar value) { + super.setBar(value); + } + + /* + * Overridden “standard” read accessor. + * + * We override it to ensure it returns the more specialised subtype. + * + * Here, since we know the slot cannot be further refined (no subclass), we can + * dispense with a wildcard generic. + */ + @Override + @SuppressWarnings("unchecked") + public List getBars() { + return (List) super.getBars(); + } + + /* + * Overriden read accessor with optional creation of the list. + * + * We must override this accessor to ensure that the created list (if the list + * needs to be created) is using the more specialised type. + */ + @Override + public List getBars(boolean create) { + // We can delegate the logic to the parent + return super.getBars(SecondDerivedBar.class, create); + } + + /* + * Overidden parameterised read accessor. + * + * In this class, we don’t need this accessor to get a modifiable list (we can + * use `getBars()` directly), but we must still override the accessor we inherit + * from the parent, otherwise this would allow client code to get a + * `List`-typed value. + */ + @Override + public List getBars(Class t) { + if ( !SecondDerivedBar.class.isAssignableFrom(t) ) { + throw new IllegalArgumentException("Invalid type parameter"); + } + return super.getBars(t); + } + + /* + * Overridden parameterised read accessor with optional creation of the list. + * + * Same as above: we must override this accessor to add a runtime check on the + * type parameter. + */ + @Override + public List getBars(Class t, boolean create) { + if ( !SecondDerivedBar.class.isAssignableFrom(t) ) { + throw new IllegalArgumentException("Invalid type parameter"); + } + return super.getBars(t, create); + } + + /* + * Overridden “standard” write accessor. + * + * We must override this accessor to include a runtime check. The check must be + * performed on all list items. + */ + @Override + public void setBars(List value) { + for ( Bar b : value ) { + if ( !(b instanceof SecondDerivedBar) ) { + throw new IllegalArgumentException("Invalid bar value"); + } + } + // FIXME: the parent method will in turn perform a (no longer needed) runtime + // check... + super.setBars(value); + } +} From f70472c3fc5485feefa8dc14a3763f7c61520582 Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Wed, 27 May 2026 18:17:07 +0100 Subject: [PATCH 2/2] Detect refined inherited slots. Assuming LinkML-derived Java classes would use the model we are currently envisionning to deal with refined inherited slots, this commit updates the Slot class to use the generated code to automatically detect whether an inherited slot in a derived class has been refined compared to the original definition of the slot. We do that by looking for an overriding read accessor in any of the classes between the slot-defining class and the final class. If there is such an overriding accessor, we take its return type as the refined type for the slot. --- .../java/org/incenp/linkml/core/Slot.java | 62 ++++++++++++++++--- .../linkml/core/playground/Playground.java | 51 ++++++++++----- 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/incenp/linkml/core/Slot.java b/core/src/main/java/org/incenp/linkml/core/Slot.java index f99df56..1e0e4fd 100644 --- a/core/src/main/java/org/incenp/linkml/core/Slot.java +++ b/core/src/main/java/org/incenp/linkml/core/Slot.java @@ -38,6 +38,8 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -79,13 +81,30 @@ public class Slot { * fide LinkML object). */ public Slot(Field field) throws LinkMLRuntimeException { + this(field, field.getDeclaringClass()); + } + + /** + * Creates a new instance. + * + * @param field The Java field that represents the slot. + * @param klass The class to which the slot belongs. + * @throws LinkMLRuntimeException If neither the class the field belongs to or + * any of its parents declare accessor methods + * for the slot (which should not happen if the + * class is a bona fide LinkML object). + */ + public Slot(Field field, Class klass) throws LinkMLRuntimeException { this.field = field; - outerType = field.getType(); - Class klass = field.getDeclaringClass(); try { - writeAccessor = klass.getDeclaredMethod(getWriteAccessorName(field), new Class[] { outerType }); - readAccessor = klass.getDeclaredMethod(getReadAccessorName(field), (Class[]) null); + readAccessor = klass.getMethod(getReadAccessorName(field), (Class[]) null); + } catch ( NoSuchMethodException | SecurityException e ) { + throw new LinkMLInternalError(String.format("Missing accessor for slot '%s'", field.getName()), e); + } + outerType = readAccessor.getReturnType(); + try { + writeAccessor = klass.getMethod(getWriteAccessorName(field), new Class[] { outerType }); } catch ( NoSuchMethodException | SecurityException e ) { throw new LinkMLInternalError(String.format("Missing accessor for slot '%s'", field.getName()), e); } @@ -210,8 +229,13 @@ public boolean isCurieTyped() { */ public Class getInnerType() { if ( isMultivalued() ) { - ParameterizedType pt = (ParameterizedType) field.getGenericType(); - return (Class) pt.getActualTypeArguments()[0]; + ParameterizedType pt = (ParameterizedType) readAccessor.getGenericReturnType(); + Type t = pt.getActualTypeArguments()[0]; + if ( t instanceof WildcardType ) { + return (Class) ((WildcardType) t).getUpperBounds()[0]; + } else { + return (Class) t; + } } return outerType; } @@ -327,6 +351,28 @@ public Class getDeclaringClass() { return field.getDeclaringClass(); } + /** + * Gets the class in which the slot is refined, if any. + *

+ * In LinkML, a class that inherit a slot from one of its parents can + * refine that slot by restricting its range to a more specific class + * than the original range. + *

+ * In this runtime, we detect this by looking for a read accessor in one of the + * derived classes, that overrides the original read accessor in the class that + * defines the slot. + *

+ * If the slot is refined by several classes successively, this method will + * return the last refining class. + * + * @return The class that refines the slot, or null if the slot is + * not refined relatively to its declaring class. + */ + public Class getRefiningClass() { + Class klass = readAccessor.getDeclaringClass(); + return klass != getDeclaringClass() ? klass : null; + } + /** * Assigns a value to the slot for the given object. * @@ -440,7 +486,7 @@ public static Slot getSlot(Class klass, String name) throws LinkMLRuntimeExce do { try { Field f = current.getDeclaredField(name); - return new Slot(f); + return new Slot(f, klass); } catch ( NoSuchFieldException e ) { } @@ -463,7 +509,7 @@ public static Collection getSlots(Class klass) { do { for ( Field f : current.getDeclaredFields() ) { try { - slots.add(new Slot(f)); + slots.add(new Slot(f, klass)); } catch ( LinkMLRuntimeException e ) { // Assume this is not a LinkML field } diff --git a/core/src/test/java/org/incenp/linkml/core/playground/Playground.java b/core/src/test/java/org/incenp/linkml/core/playground/Playground.java index d137b8d..ca3652a 100644 --- a/core/src/test/java/org/incenp/linkml/core/playground/Playground.java +++ b/core/src/test/java/org/incenp/linkml/core/playground/Playground.java @@ -1,9 +1,7 @@ package org.incenp.linkml.core.playground; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.WildcardType; - +import org.incenp.linkml.core.LinkMLRuntimeException; +import org.incenp.linkml.core.Slot; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -99,17 +97,40 @@ void testThirdDerivedFoo() { } @Test - void testObtainingParameterBound() { - Class klass = FirstDerivedFoo.class; - Method m = null; - try { - m = klass.getDeclaredMethod("getBars", (Class[]) null); - } catch ( NoSuchMethodException | SecurityException e ) { - Assertions.fail("No getBars method"); - } + void testGetRefinedSingleValuedType() throws LinkMLRuntimeException { + Slot barSlot = Slot.getSlot(Foo.class, "bar"); + Assertions.assertEquals(Bar.class, barSlot.getInnerType()); + Assertions.assertNull(barSlot.getRefiningClass()); + + barSlot = Slot.getSlot(FirstDerivedFoo.class, "bar"); + Assertions.assertEquals(FirstDerivedBar.class, barSlot.getInnerType()); + Assertions.assertEquals(FirstDerivedFoo.class, barSlot.getRefiningClass()); + + barSlot = Slot.getSlot(SecondDerivedFoo.class, "bar"); + Assertions.assertEquals(FirstDerivedBar.class, barSlot.getInnerType()); + Assertions.assertEquals(FirstDerivedFoo.class, barSlot.getRefiningClass()); + + barSlot = Slot.getSlot(ThirdDerivedFoo.class, "bar"); + Assertions.assertEquals(SecondDerivedBar.class, barSlot.getInnerType()); + Assertions.assertEquals(ThirdDerivedFoo.class, barSlot.getRefiningClass()); + } - ParameterizedType t = (ParameterizedType) m.getGenericReturnType(); - WildcardType t2 = (WildcardType) t.getActualTypeArguments()[0]; - Assertions.assertEquals(FirstDerivedBar.class, t2.getUpperBounds()[0]); + @Test + void testGetRefinedMultiValuedType() throws LinkMLRuntimeException { + Slot barSlot = Slot.getSlot(Foo.class, "bars"); + Assertions.assertEquals(Bar.class, barSlot.getInnerType()); + Assertions.assertNull(barSlot.getRefiningClass()); + + barSlot = Slot.getSlot(FirstDerivedFoo.class, "bars"); + Assertions.assertEquals(FirstDerivedBar.class, barSlot.getInnerType()); + Assertions.assertEquals(FirstDerivedFoo.class, barSlot.getRefiningClass()); + + barSlot = Slot.getSlot(SecondDerivedFoo.class, "bars"); + Assertions.assertEquals(FirstDerivedBar.class, barSlot.getInnerType()); + Assertions.assertEquals(FirstDerivedFoo.class, barSlot.getRefiningClass()); + + barSlot = Slot.getSlot(ThirdDerivedFoo.class, "bars"); + Assertions.assertEquals(SecondDerivedBar.class, barSlot.getInnerType()); + Assertions.assertEquals(ThirdDerivedFoo.class, barSlot.getRefiningClass()); } }