[stdlib] Improve EnumEntries

* Provide short-circuit methods for contract "EnumEntries contains all
  enum entries"
* Make EnumEntries serializable
* Introduce more convenient overload for other backends

#KT-53152


Merge-request: KT-MR-6867
Merged-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
This commit is contained in:
Vsevolod Tolstopyatov
2022-08-22 16:50:28 +00:00
committed by Space
parent cc00dcc038
commit 5054e301be
10 changed files with 209 additions and 21 deletions
@@ -48,7 +48,7 @@ val commonMainSources by task<Sync> {
"libraries/stdlib/src/kotlin/time/**",
"libraries/stdlib/src/kotlin/util/KotlinVersion.kt",
"libraries/stdlib/src/kotlin/util/Tuples.kt",
"libraries/stdlib/src/kotlin/enums/EnumEntries.kt"
"libraries/stdlib/src/kotlin/enums/**"
)
)
fullCommonMainSources.outputs.files.singleFile
@@ -96,7 +96,8 @@ val jsMainSources by task<Sync> {
"libraries/stdlib/js/src/kotlin/dom/**",
"libraries/stdlib/js/src/kotlin/browser/**",
"libraries/stdlib/js/src/kotlinx/dom/**",
"libraries/stdlib/js/src/kotlinx/browser/**"
"libraries/stdlib/js/src/kotlinx/browser/**",
"libraries/stdlib/js/src/kotlin/enums/**"
)
)
fullJsMainSources.outputs.files.singleFile
@@ -0,0 +1,9 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.enums
// Unused stub
internal actual class EnumEntriesSerializationProxy<E : Enum<E>> actual constructor(entries: Array<E>)
@@ -0,0 +1,20 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.enums
@Suppress("UNCHECKED_CAST", "unused")
internal actual class EnumEntriesSerializationProxy<E : Enum<E>> actual constructor(entries: Array<E>) : Serializable {
private val c: Class<E> = entries.javaClass.componentType!! as Class<E>
private companion object {
private const val serialVersionUID: Long = 0L
}
@OptIn(ExperimentalStdlibApi::class)
private fun readResolve(): Any {
return enumEntries(c.enumConstants)
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
package test.enums
import org.junit.Test
import test.collections.behaviors.listBehavior
import test.collections.compare
import test.io.deserializeFromByteArray
import test.io.serializeAndDeserialize
import kotlin.enums.EnumEntries
import kotlin.enums.enumEntries
import kotlin.test.assertEquals
@Suppress("UNUSED_EXPRESSION")
class EnumEntriesJvmTest {
enum class EmptyEnum
enum class NonEmptyEnum {
A, B, C
}
@Test
fun testEmptyEnumSerialization() {
val entries = serializeAndDeserialize(enumEntries(EmptyEnum::values))
compare(EmptyEnum.values().toList(), entries) { listBehavior() }
}
@Test
fun testNonEmptyEnumSerialization() {
val entries = serializeAndDeserialize(enumEntries(NonEmptyEnum::values))
compare(NonEmptyEnum.values().toList(), entries) { listBehavior() }
}
@Test
fun testLambdaIsNotSerialized() {
val nonSerializable = object {} // Deliberately non-serializable
val entries = enumEntries {
nonSerializable // Capture it
EmptyEnum.values()
}
val newEntries = serializeAndDeserialize(entries)
assertEquals(entries, newEntries)
}
// Declarations for enum evolution test
/*
* This is the serialized contents of
* ```
* enum class Evolved {}
* ```
* without ANY entries.
*/
private val bytes =
("-84,-19,0,5,115,114,0,42,107,111,116,108,105,110,46,101,110,117,109,115,46,69,110,117,109,69,110,116,114," +
"105,101,115,83,101,114,105,97,108,105,122,97,116,105,111,110,80,114,111,120,121,0,0,0,0,0,0,0,0,2," +
"0,1,76,0,1,99,116,0,17,76,106,97,118,97,47,108,97,110,103,47,67,108,97,115,115,59,120,112,118,114,0," +
"37,116,101,115,116,46,101,110,117,109,115,46,69,110,117,109,69,110,116,114,105,101,115,74,118,109,84," +
"101,115,116,36,69,118,111,108,118,101,100,0,0,0,0,0,0,0,0,18,0,0,120,114,0,14,106,97,118,97,46,108,97," +
"110,103,46,69,110,117,109,0,0,0,0,0,0,0,0,18,0,0,120,112")
.split(",").map { it.toInt().toByte() }.toByteArray()
// Emulate enum evolution
enum class Evolved {
E1, E2, E3
}
@Test
fun testEnumEvolution() {
// Test checks that if the enum has new members after being serialized, they are all still deserialized properly
val list = deserializeFromByteArray<EnumEntries<Evolved>>(bytes)
assertEquals(enumEntries(Evolved::values), list)
}
}
@@ -0,0 +1,9 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.enums
// Unused stub
internal actual class EnumEntriesSerializationProxy<E : Enum<E>> actual constructor(entries: Array<E>)
+4
View File
@@ -51,6 +51,10 @@ Low-level building blocks for libraries that provide coroutine-based APIs.
Utility functions for working with the browser DOM.
# Package kotlin.enum
Utilities for working with Kotlin enum classes.
# Package kotlin.experimental
Experimental APIs, subject to change in future versions of Kotlin.
@@ -21,9 +21,14 @@ public sealed interface EnumEntries<E : Enum<E>> : List<E>
@PublishedApi
@ExperimentalStdlibApi
@SinceKotlin("1.8")
@SinceKotlin("1.8") // Used by JVM compiler
internal fun <E : Enum<E>> enumEntries(entriesProvider: () -> Array<E>): EnumEntries<E> = EnumEntriesList(entriesProvider)
@PublishedApi
@ExperimentalStdlibApi
@SinceKotlin("1.8") // Used by Native/JS compilers and Java serialization
internal fun <E : Enum<E>> enumEntries(entries: Array<E>): EnumEntries<E> = EnumEntriesList { entries }
/*
* For enum class E, this class is instantiated in the following manner (NB it's pseudocode that does not
* reflect code generation strategy precisely):
@@ -55,24 +60,7 @@ internal fun <E : Enum<E>> enumEntries(entriesProvider: () -> Array<E>): EnumEnt
*/
@SinceKotlin("1.8")
@ExperimentalStdlibApi
private class EnumEntriesList<E : Enum<E>>(private val entriesProvider: () -> Array<E>) : EnumEntries<E>, AbstractList<E>() {
/*
* Open questions to implementation:
*
* - Are we allowed to use e.ordinal as an index?
* - e.g. indexOf(e) = e.ordinal
*
* - Are we allowed to short-circuit methods?
* - e.g. `EEL.contains(anyE)` is always true as long as no reflection is involved
*
* - Should it be Java-serializable? (then we definitely can suffer from short-circuiting and should be extra-careful around read-resolve)
*
* - Should it be sealed or just a class with internal constructor? TODO discuss on design to align this policy over all the language
* - Probably should to avoid exposing AbstractList superclass directly?
*
* - TODO package-info for kotlinlang
*/
private class EnumEntriesList<E : Enum<E>>(private val entriesProvider: () -> Array<E>) : EnumEntries<E>, AbstractList<E>(), Serializable {
@Volatile // Volatile is required for safe publication of the array. It doesn't incur any real-world penalties
private var _entries: Array<E>? = null
@@ -93,4 +81,33 @@ private class EnumEntriesList<E : Enum<E>>(private val entriesProvider: () -> Ar
checkElementIndex(index, entries.size)
return entries[index]
}
// By definition, EnumEntries contains **all** enums in declaration order,
// thus we are able to short-circuit the implementation here
override fun contains(element: E): Boolean {
@Suppress("SENSELESS_COMPARISON")
if (element === null) return false // WA for JS IR bug
// Check identity due to UnsafeVariance
val target = entries.getOrNull(element.ordinal)
return target === element
}
override fun indexOf(element: E): Int {
@Suppress("SENSELESS_COMPARISON")
if (element === null) return -1 // WA for JS IR bug
// Check identity due to UnsafeVariance
val ordinal = element.ordinal
val target = entries.getOrNull(ordinal)
return if (target === element) ordinal else -1
}
override fun lastIndexOf(element: E): Int = indexOf(element)
@Suppress("unused") // Used for Java serialization
private fun writeReplace(): Any {
return EnumEntriesSerializationProxy(entries)
}
}
internal expect class EnumEntriesSerializationProxy<E : Enum<E>>(entries: Array<E>)
@@ -24,6 +24,18 @@ public fun <T> CompareContext<List<T>>.listBehavior() {
propertyEquals { indexOf(elementAtOrNull(0)) }
propertyEquals { lastIndexOf(elementAtOrNull(0)) }
for (element in expected) {
propertyEquals { this.indexOf(element) }
propertyEquals { this.lastIndexOf(element) }
}
val nonExisting = object {}
propertyEquals { this.indexOf(nonExisting as Any?) }
propertyEquals { this.lastIndexOf(nonExisting as Any?) }
propertyEquals { this.indexOf(null as Any?) }
propertyEquals { this.lastIndexOf(null as Any?) }
propertyFails { subList(0, size + 1) }
propertyFails { subList(-1, 0) }
propertyEquals { subList(0, size) }
@@ -18,6 +18,12 @@ class EnumEntriesListTest {
A, B, C
}
@Test
fun testCannotBeCasted() {
val list = enumEntries(EmptyEnum::values)
assertTrue { list !is MutableList<*> }
}
@Test
fun testForEmptyEnum() {
val list = enumEntries(EmptyEnum::values)
@@ -54,4 +60,33 @@ class EnumEntriesListTest {
val list = enumEntries(NonEmptyEnum::values)
compare(NonEmptyEnum.values().toList(), list) { listBehavior() }
}
enum class E1 {
A
}
enum class E2 {
A, B
}
@Test
fun testVariantEnumBehaviour() {
val list = enumEntries(E1::values)
// Index of
val enumList: List<Enum<*>> = list
assertEquals(0, enumList.indexOf(E1.A))
assertEquals(-1, enumList.indexOf(E2.A))
assertEquals(-1, enumList.indexOf(E2.B))
// Last index of
assertEquals(0, enumList.lastIndexOf(E1.A))
assertEquals(-1, enumList.lastIndexOf(E2.A))
assertEquals(-1, enumList.lastIndexOf(E2.B))
// Contains
assertTrue(enumList.contains(E1.A))
assertFalse(enumList.contains(E2.A))
assertFalse(enumList.contains(E2.B))
}
}
@@ -3143,6 +3143,7 @@ public abstract interface class kotlin/enums/EnumEntries : java/util/List, kotli
public final class kotlin/enums/EnumEntriesKt {
public static final fun enumEntries (Lkotlin/jvm/functions/Function0;)Lkotlin/enums/EnumEntries;
public static final fun enumEntries ([Ljava/lang/Enum;)Lkotlin/enums/EnumEntries;
}
public abstract interface annotation class kotlin/experimental/ExperimentalObjCName : java/lang/annotation/Annotation {