Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,62 @@ data class DynamicType(
override val creationPermitted: Boolean = true,
) : DomainType {

/**
* A dynamic type has no JVM class, so no class can be assignable to it.
* (Unchanged: whether a dynamic type declaring a JVM parent should be
* assignable TO that `Class` is a separate question — see [isAssignableTo].)
*/
override fun isAssignableFrom(other: Class<*>): Boolean = false

override fun isAssignableFrom(other: DomainType): Boolean = other.name == name
/**
* True when [other] IS this type or DECLARES it as an ancestor, transitively.
*
* [parents] existed but was never consulted here, so a declared hierarchy
* conferred no subtyping: with `Employee -> Person`, `Person.isAssignableFrom(Employee)`
* was false. [JvmType] has always walked the real class hierarchy, so the two
* halves of `DomainType` disagreed about what inheritance means.
*/
override fun isAssignableFrom(other: DomainType): Boolean = other.isAssignableTo(this)

/** A dynamic type has no JVM class, so it is assignable to none. */
override fun isAssignableTo(other: Class<*>): Boolean = false

override fun isAssignableTo(other: DomainType): Boolean = other.name == name
/**
* True when this type IS [other] or DECLARES it as an ancestor, transitively.
*
* An ancestor may be a [JvmType] — a realm declaring `parents: [Signal]` in its
* type YAML — in which case that ancestor's own (class-hierarchy) assignability
* decides, so a type declaring `Signal` is also assignable to `Signal`'s
* supertypes.
*/
override fun isAssignableTo(other: DomainType): Boolean =
selfAndAncestors().any { ancestor ->
// A dynamic ancestor matches by name (the identity dynamic types have);
// a JVM ancestor delegates to its own class-hierarchy walk.
if (ancestor is DynamicType) ancestor.name == other.name else ancestor.isAssignableTo(other)
}

/**
* This type and every ancestor reachable through [parents], nearest first.
*
* Iterative and de-duplicated by name, so a hand-built cyclic chain terminates
* instead of overflowing the stack — nothing stops a caller constructing one.
* Only DYNAMIC parents are expanded: a [JvmType] ancestor answers for its own
* hierarchy above, and expanding it here would reflectively load every
* superclass and interface for a question they can answer themselves.
*/
private fun selfAndAncestors(): Collection<DomainType> {
val seen = LinkedHashMap<String, DomainType>()
val queue = ArrayDeque<DomainType>()
queue.add(this)
while (queue.isNotEmpty()) {
val next = queue.removeFirst()
if (seen.containsKey(next.name)) continue
seen[next.name] = next
if (next is DynamicType) queue.addAll(next.parents)
}
return seen.values
}

override fun children(additionalBasePackages: Collection<String>): Collection<DomainType> {
// Dynamic types don't have classpath descendants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ data class JvmType @JsonCreator constructor(
override fun isAssignableFrom(other: DomainType): Boolean =
when (other) {
is JvmType -> clazz.isAssignableFrom(other.clazz)
is DynamicType -> false
// A dynamic type may DECLARE a JVM parent (a realm's `parents: [Signal]`).
// Delegating keeps the invariant `a.isAssignableFrom(b) == b.isAssignableTo(a)`,
// which a bare `false` here would break once DynamicType walks its parents.
is DynamicType -> other.isAssignableTo(this)
}

override fun isAssignableTo(other: Class<*>): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,124 @@ class DomainTypeAssignabilityTest {
assertFalse(jvmType.isAssignableTo(dynamicType))
}
}

/**
* `DynamicType.parents` existed but was never consulted by assignability, so a
* declared hierarchy conferred no subtyping while [JvmType] walked the real class
* hierarchy — the two halves of `DomainType` disagreed about what inheritance means.
*
* Every case here involves a type WITH parents; the parentless behaviour every other
* test in this class pins is unchanged.
*/
@Nested
inner class DynamicTypeInheritance {

private val party = DynamicType(name = "Party")
private val person = DynamicType(name = "Person", parents = listOf(party))
private val employee = DynamicType(name = "Employee", parents = listOf(person))
private val widget = DynamicType(name = "Widget")

@Test
fun `a declared parent is assignable from its child`() {
assertTrue(person.isAssignableFrom(employee))
assertTrue(employee.isAssignableTo(person))
}

@Test
fun `a grandparent is assignable from its grandchild`() {
assertTrue(party.isAssignableFrom(employee))
assertTrue(employee.isAssignableTo(party))
}

@Test
fun `assignability is not symmetric between parent and child`() {
assertFalse(employee.isAssignableFrom(person))
assertFalse(person.isAssignableTo(employee))
}

@Test
fun `an unrelated type is still not assignable`() {
assertFalse(widget.isAssignableFrom(employee))
assertFalse(employee.isAssignableTo(widget))
}

@Test
fun `multiple parents all confer assignability`() {
val payable = DynamicType(name = "Payable")
val watchable = DynamicType(name = "Watchable")
val donor = DynamicType(name = "Donor", parents = listOf(payable, watchable))

assertTrue(payable.isAssignableFrom(donor))
assertTrue(watchable.isAssignableFrom(donor))
}

@Test
fun `a diamond resolves without duplication or error`() {
val contractor = DynamicType(name = "Contractor", parents = listOf(party))
val secondee = DynamicType(name = "Secondee", parents = listOf(employee, contractor))

assertTrue(party.isAssignableFrom(secondee))
assertTrue(person.isAssignableFrom(secondee))
assertTrue(contractor.isAssignableFrom(secondee))
}

/** Nothing stops a caller building one, so the walk must not overflow the stack. */
@Test
fun `a cyclic parent chain terminates`() {
val a = DynamicType(name = "A")
val b = DynamicType(name = "B", parents = listOf(a))
val cyclicA = a.copy(parents = listOf(b))

assertTrue(cyclicA.isAssignableTo(b))
assertTrue(b.isAssignableTo(cyclicA))
assertFalse(cyclicA.isAssignableTo(widget))
}

/**
* A dynamic type may declare a JVM parent — how a non-JVM type says it is a
* `Signal`. The JVM ancestor answers for its own hierarchy, so the child is
* assignable to that parent's supertypes too.
*/
@Test
fun `a declared JVM parent confers assignability, including its supertypes`() {
val implementing = JvmType(ImplementingClass::class.java)
val dynamic = DynamicType(name = "DeclaredOnTop", parents = listOf(implementing))

assertTrue(dynamic.isAssignableTo(implementing))
assertTrue(dynamic.isAssignableTo(JvmType(TestInterface::class.java)))
assertFalse(dynamic.isAssignableTo(JvmType(ConcreteBase::class.java)))
}

/** `a.isAssignableFrom(b)` must agree with `b.isAssignableTo(a)` across kinds. */
@Test
fun `a JvmType is assignable from a dynamic type that declares it`() {
val implementing = JvmType(ImplementingClass::class.java)
val dynamic = DynamicType(name = "DeclaredOnTop", parents = listOf(implementing))

assertTrue(implementing.isAssignableFrom(dynamic))
assertTrue(JvmType(TestInterface::class.java).isAssignableFrom(dynamic))
assertFalse(JvmType(ConcreteBase::class.java).isAssignableFrom(dynamic))
}

/** A JVM class never declares a dynamic parent, so this stays false. */
@Test
fun `a JvmType is never assignable TO a dynamic type`() {
val implementing = JvmType(ImplementingClass::class.java)
val dynamic = DynamicType(name = "DeclaredOnTop", parents = listOf(implementing))

assertFalse(implementing.isAssignableTo(dynamic))
}

/** The Class overloads are deliberately untouched: a dynamic type has no class. */
@Test
fun `a declared JVM parent does not make the Class overloads true`() {
val dynamic = DynamicType(
name = "DeclaredOnTop",
parents = listOf(JvmType(ImplementingClass::class.java)),
)

assertFalse(dynamic.isAssignableTo(ImplementingClass::class.java))
assertFalse(dynamic.isAssignableFrom(ImplementingClass::class.java))
}
}
}
Loading