[kotlin-tooling-core] Allow nullable extra values

The change is binary compatible, but not source compatible.
Potential clients like Google might need to adapt
a potential `IdeaKotlinExtrasSerializationExtension` implementaiton.

Right now, no such implementation is known
This commit is contained in:
Sebastian Sellmair
2024-02-21 13:35:41 +01:00
committed by Space Team
parent 622a88ff26
commit 704379934e
12 changed files with 142 additions and 53 deletions
@@ -11,9 +11,11 @@ import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import org.jetbrains.kotlin.gradle.idea.proto.generated.IdeaExtrasProto
import org.jetbrains.kotlin.gradle.idea.proto.generated.ideaExtrasProto
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinExtrasSerializer
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinSerializationContext
import org.jetbrains.kotlin.tooling.core.*
import org.jetbrains.kotlin.tooling.core.Extras
import org.jetbrains.kotlin.tooling.core.MutableExtras
import org.jetbrains.kotlin.tooling.core.toMutableExtras
import org.jetbrains.kotlin.tooling.core.withValue
fun Extras.toByteArray(context: IdeaKotlinSerializationContext): ByteArray {
return context.IdeaExtrasProto(this).toByteArray()
@@ -35,8 +37,8 @@ internal fun IdeaKotlinSerializationContext.IdeaExtrasProto(extras: Extras): Ide
val context = this
return ideaExtrasProto {
extras.entries.forEach { (key, value) ->
key as Extras.Key<Any?>
val serializer = context.extrasSerializationExtension.serializer(key) ?: return@forEach
serializer as IdeaKotlinExtrasSerializer<Any>
val serialized = runCatching { serializer.serialize(context, value) ?: return@forEach }.getOrElse { exception ->
logger.error("Failed to serialize $key, using ${serializer.javaClass.simpleName}", exception)
return@forEach
@@ -18,10 +18,10 @@ import org.jetbrains.kotlin.tooling.core.Extras
* inside the implementation of the given [IdeaKotlinExtrasSerializer]
*/
interface IdeaKotlinExtrasSerializationExtension {
fun <T : Any> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>?
fun <T> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>?
object Empty : IdeaKotlinExtrasSerializationExtension {
override fun <T : Any> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>? = null
override fun <T> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>? = null
}
}
@@ -48,7 +48,7 @@ private class IdeaKotlinExtrasSerializationExtensionBuilderImpl : IdeaKotlinExtr
private class IdeaKotlinExtrasSerializationExtensionImpl(
private val map: Map<Extras.Key<*>, IdeaKotlinExtrasSerializer<*>>
) : IdeaKotlinExtrasSerializationExtension {
override fun <T : Any> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>? {
override fun <T> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>? {
@Suppress("unchecked_cast")
return map[key] as? IdeaKotlinExtrasSerializer<T>
}
@@ -8,7 +8,7 @@ package org.jetbrains.kotlin.gradle.idea.serialize
import kotlin.reflect.KClass
interface IdeaKotlinExtrasSerializer<T : Any> {
interface IdeaKotlinExtrasSerializer<T> {
fun serialize(context: IdeaKotlinSerializationContext, value: T): ByteArray?
fun deserialize(context: IdeaKotlinSerializationContext, data: ByteArray): T?
@@ -16,7 +16,7 @@ object TestIdeaExtrasSerializationExtension : IdeaKotlinExtrasSerializationExten
val ignoredStringKey = extrasKeyOf<String>("ignored")
val anySerializableKey = extrasKeyOf<Any>("serializable")
override fun <T : Any> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>? = when {
override fun <T> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>? = when {
key == ignoredStringKey -> null
key == anySerializableKey -> IdeaKotlinExtrasSerializer.javaIoSerializable<Any>()
key.type == extrasTypeOf<String>() -> IdeaKotlinStringExtrasSerializer
@@ -6,14 +6,14 @@
package org.jetbrains.kotlin.gradle.plugin.ide
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinExtrasSerializer
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinExtrasSerializationExtension
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinExtrasSerializer
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinSerializationContext
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinSerializationLogger
import org.jetbrains.kotlin.tooling.core.Extras
internal fun IdeaKotlinSerializationContext(
logger: Logger, extrasSerializationExtensions: List<IdeaKotlinExtrasSerializationExtension>
logger: Logger, extrasSerializationExtensions: List<IdeaKotlinExtrasSerializationExtension>,
): IdeaKotlinSerializationContext {
return IdeaKotlinSerializationContextImpl(
extrasSerializationExtension = IdeaKotlinExtrasSerializationExtensionImpl(logger, extrasSerializationExtensions),
@@ -23,7 +23,7 @@ internal fun IdeaKotlinSerializationContext(
private class IdeaKotlinSerializationContextImpl(
override val extrasSerializationExtension: IdeaKotlinExtrasSerializationExtension,
override val logger: IdeaKotlinSerializationLogger
override val logger: IdeaKotlinSerializationLogger,
) : IdeaKotlinSerializationContext
@@ -31,9 +31,9 @@ private class IdeaKotlinSerializationContextImpl(
private class IdeaKotlinExtrasSerializationExtensionImpl(
private val logger: Logger,
private val extensions: List<IdeaKotlinExtrasSerializationExtension>
private val extensions: List<IdeaKotlinExtrasSerializationExtension>,
) : IdeaKotlinExtrasSerializationExtension {
override fun <T : Any> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>? {
override fun <T> serializer(key: Extras.Key<T>): IdeaKotlinExtrasSerializer<T>? {
val serializers = extensions.mapNotNull { it.serializer(key) }
if (serializers.size == 1) {
@@ -127,6 +127,7 @@ public final class org/jetbrains/kotlin/tooling/core/ExtrasUtilsKt {
public static final fun extrasOf ()Lorg/jetbrains/kotlin/tooling/core/Extras;
public static final fun extrasOf ([Lorg/jetbrains/kotlin/tooling/core/Extras$Entry;)Lorg/jetbrains/kotlin/tooling/core/Extras;
public static final fun getOrPut (Lorg/jetbrains/kotlin/tooling/core/MutableExtras;Lorg/jetbrains/kotlin/tooling/core/Extras$Key;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public static final fun getOrPutNullable (Lorg/jetbrains/kotlin/tooling/core/MutableExtras;Lorg/jetbrains/kotlin/tooling/core/Extras$Key;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public static final fun mutableExtrasOf ()Lorg/jetbrains/kotlin/tooling/core/MutableExtras;
public static final fun mutableExtrasOf ([Lorg/jetbrains/kotlin/tooling/core/Extras$Entry;)Lorg/jetbrains/kotlin/tooling/core/MutableExtras;
public static final fun plus (Lorg/jetbrains/kotlin/tooling/core/Extras;Ljava/lang/Iterable;)Lorg/jetbrains/kotlin/tooling/core/Extras;
@@ -62,9 +62,9 @@ import java.io.Serializable
* .toExtras()
* ```
*/
interface Extras : Collection<Entry<out Any>> {
interface Extras : Collection<Entry<*>> {
class Type<T> @UnsafeApi("Use 'extrasTypeOf()' instead") @PublishedApi internal constructor(
internal val signature: String
internal val signature: String,
) : Serializable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -85,7 +85,7 @@ interface Extras : Collection<Entry<out Any>> {
}
/* Not implemented as data class to ensure more controllable binary compatibility */
class Key<T : Any> @PublishedApi internal constructor(
class Key<T> @PublishedApi internal constructor(
val type: Type<T>,
val name: String? = null,
) : Serializable {
@@ -116,8 +116,8 @@ interface Extras : Collection<Entry<out Any>> {
fun fromString(stableString: String): Key<*> {
@OptIn(UnsafeApi::class) return if (stableString.contains(';')) {
val split = stableString.split(';', limit = 2)
Key(Type(split[0]), split[1])
} else Key(Type(stableString))
Key<Any?>(Type(split[0]), split[1])
} else Key<Any?>(Type(stableString))
}
private const val serialVersionUID = 0L
@@ -125,7 +125,7 @@ interface Extras : Collection<Entry<out Any>> {
}
/* Not implemented as data class to ensure more controllable binary compatibility */
class Entry<T : Any>(val key: Key<T>, val value: T) : Serializable {
class Entry<T>(val key: Key<T>, val value: T) : Serializable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Entry<*>) return false
@@ -152,22 +152,22 @@ interface Extras : Collection<Entry<out Any>> {
val keys: Set<Key<*>>
val entries: Set<Entry<*>>
operator fun <T : Any> get(key: Key<T>): T?
operator fun <T> get(key: Key<T>): T?
operator fun contains(key: Key<*>): Boolean
override fun iterator(): Iterator<Entry<out Any>>
override fun iterator(): Iterator<Entry<*>>
}
interface MutableExtras : Extras {
/**
* @return The previous value or null if no previous value was set
*/
operator fun <T : Any> set(key: Key<T>, value: T): T?
operator fun <T> set(key: Key<T>, value: T): T?
fun <T : Any> put(entry: Entry<T>): T?
fun <T> put(entry: Entry<T>): T?
fun putAll(from: Iterable<Entry<*>>)
fun <T : Any> remove(key: Key<T>): T?
fun <T> remove(key: Key<T>): T?
fun clear()
}
@@ -11,7 +11,7 @@ import java.io.Serializable
@Suppress("unchecked_cast")
internal class MutableExtrasImpl(
initialEntries: Iterable<Entry<*>> = emptyList()
initialEntries: Iterable<Entry<*>> = emptyList(),
) : MutableExtras, AbstractExtras(), Serializable {
private val extras: MutableMap<Key<*>, Entry<*>> =
@@ -28,11 +28,11 @@ internal class MutableExtrasImpl(
override fun isEmpty(): Boolean = extras.isEmpty()
override fun <T : Any> set(key: Key<T>, value: T): T? {
override fun <T> set(key: Key<T>, value: T): T? {
return put(Entry(key, value))
}
override fun <T : Any> put(entry: Entry<T>): T? {
override fun <T> put(entry: Entry<T>): T? {
return extras.put(entry.key, entry)?.let { it.value as T }
}
@@ -40,11 +40,11 @@ internal class MutableExtrasImpl(
this.extras.putAll(from.associateBy { it.key })
}
override fun <T : Any> get(key: Key<T>): T? {
override fun <T> get(key: Key<T>): T? {
return extras[key]?.let { it.value as T }
}
override fun <T : Any> remove(key: Key<T>): T? {
override fun <T> remove(key: Key<T>): T? {
return extras.remove(key)?.let { it.value as T }
}
@@ -59,7 +59,7 @@ internal class MutableExtrasImpl(
@Suppress("unchecked_cast")
internal class ImmutableExtrasImpl private constructor(
private val extras: Map<Key<*>, Entry<*>>
private val extras: Map<Key<*>, Entry<*>>,
) : AbstractExtras(), Serializable {
constructor(extras: Iterable<Entry<*>>) : this(extras.associateBy { it.key })
@@ -73,7 +73,7 @@ internal class ImmutableExtrasImpl private constructor(
override val entries: Set<Entry<*>> = extras.values.toSet()
override fun <T : Any> get(key: Key<T>): T? {
override fun <T> get(key: Key<T>): T? {
return extras[key]?.let { it.value as T }
}
@@ -107,7 +107,7 @@ abstract class AbstractExtras : Extras {
override fun containsAll(elements: Collection<Entry<*>>): Boolean =
entries.containsAll(elements)
override fun iterator(): Iterator<Entry<out Any>> = entries.iterator()
override fun iterator(): Iterator<Entry<*>> = entries.iterator()
override fun equals(other: Any?): Boolean {
if (other === this) return true
@@ -135,11 +135,11 @@ internal object EmptyExtras : AbstractExtras(), Serializable {
override fun isEmpty(): Boolean = true
override fun <T : Any> get(key: Key<T>): T? = null
override fun <T> get(key: Key<T>): T? = null
override fun contains(key: Key<*>): Boolean = false
override fun contains(element: Entry<out Any>): Boolean = false
override fun contains(element: Entry<*>): Boolean = false
@Suppress("unused") // Necessary for java.io.Serializable stability
private const val serialVersionUID = 0L
@@ -11,19 +11,19 @@ import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
interface ExtrasProperty<T : Any> {
interface ExtrasProperty<T> {
val key: Extras.Key<T>
}
val <T : Any> Extras.Key<T>.readWriteProperty get() = extrasReadWriteProperty(this)
fun <Receiver : HasMutableExtras, T : Any> Extras.Key<T>.lazyProperty(factory: Receiver.() -> T) = extrasLazyProperty(this, factory)
fun <Receiver : HasMutableExtras, T> Extras.Key<T>.lazyProperty(factory: Receiver.() -> T) = extrasLazyProperty(this, factory)
fun <T : Any> extrasReadWriteProperty(key: Extras.Key<T>): ExtrasReadWriteProperty<T> = object : ExtrasReadWriteProperty<T> {
override val key: Extras.Key<T> = key
}
fun <Receiver : HasMutableExtras, T : Any> extrasLazyProperty(
fun <Receiver : HasMutableExtras, T> extrasLazyProperty(
key: Extras.Key<T>, factory: Receiver.() -> T,
): ExtrasLazyProperty<Receiver, T> =
object : ExtrasLazyProperty<Receiver, T> {
@@ -36,7 +36,7 @@ inline fun <reified T : Any> extrasReadWriteProperty(name: String? = null) =
extrasReadWriteProperty(extrasKeyOf<T>(name))
inline fun <Receiver : HasMutableExtras, reified T : Any> extrasLazyProperty(name: String? = null, noinline factory: Receiver.() -> T) =
inline fun <Receiver : HasMutableExtras, reified T> extrasLazyProperty(name: String? = null, noinline factory: Receiver.() -> T) =
extrasLazyProperty(extrasKeyOf(name), factory)
@@ -76,11 +76,11 @@ interface NotNullExtrasReadWriteProperty<T : Any> : ExtrasProperty<T>, ReadWrite
}
}
interface ExtrasLazyProperty<Receiver : HasMutableExtras, T : Any> : ExtrasProperty<T>, ReadWriteProperty<Receiver, T> {
interface ExtrasLazyProperty<Receiver : HasMutableExtras, T> : ExtrasProperty<T>, ReadWriteProperty<Receiver, T> {
val factory: Receiver.() -> T
override fun getValue(thisRef: Receiver, property: KProperty<*>): T {
return thisRef.extras.getOrPut(key) { thisRef.factory() }
return thisRef.extras.getOrPutNullable(key) { thisRef.factory() }
}
override fun setValue(thisRef: Receiver, property: KProperty<*>, value: T) {
@@ -26,7 +26,7 @@ import kotlin.reflect.typeOf
* extrasKeyOf<String>("a") != extrasKeyOf<Int>("a")
* ```
*/
inline fun <reified T : Any> extrasKeyOf(name: String? = null): Extras.Key<T> =
inline fun <reified T> extrasKeyOf(name: String? = null): Extras.Key<T> =
Extras.Key(extrasTypeOf(), name)
fun emptyExtras(): Extras = EmptyExtras
@@ -48,7 +48,7 @@ fun Iterable<Extras.Entry<*>>.toExtras(): Extras = ImmutableExtrasImpl(this)
fun Iterable<Extras.Entry<*>>.toMutableExtras(): MutableExtras = MutableExtrasImpl(this)
infix fun <T : Any> Extras.Key<T>.withValue(value: T): Extras.Entry<T> = Extras.Entry(this, value)
infix fun <T> Extras.Key<T>.withValue(value: T): Extras.Entry<T> = Extras.Entry(this, value)
operator fun Extras.plus(entry: Extras.Entry<*>): Extras = ImmutableExtrasImpl(this.entries + entry)
@@ -56,4 +56,11 @@ operator fun Extras.plus(entries: Iterable<Extras.Entry<*>>): Extras = Immutable
inline fun <T : Any> MutableExtras.getOrPut(key: Extras.Key<T>, defaultValue: () -> T): T {
return this[key] ?: defaultValue().also { this[key] = it }
}
}
inline fun <T> MutableExtras.getOrPutNullable(key: Extras.Key<T>, defaultValue: () -> T): T {
@Suppress("UNCHECKED_CAST")
if (key in this) return this[key] as T
return defaultValue().also { this[key] = it }
}
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.tooling.core
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.*
@Suppress("DEPRECATION")
@@ -18,6 +19,7 @@ class ExtrasPropertyTest {
private val keyA = extrasKeyOf<Int>("a")
private val keyB = extrasKeyOf<Int>("b")
private val keyANullable = extrasKeyOf<Int?>("a")
private val Subject.readA: Int? by keyA.readProperty
private val Subject.readB: Int? by keyB.readProperty
@@ -171,4 +173,39 @@ class ExtrasPropertyTest {
assertEquals("OK", subject2.lazyNullableString)
assertEquals(listOf(subject1, subject2), lazyNullableStringInvocations)
}
@Test
fun `test - lazyProperty - with nullable key`() {
val subject = Subject()
val invocationCounter = AtomicInteger(0)
with(object {
val Subject.lazyProperty by keyANullable.lazyProperty {
assertEquals(0, invocationCounter.getAndIncrement())
null
}
}) {
assertNull(subject.lazyProperty)
assertNull(subject.lazyProperty)
assertEquals(1, invocationCounter.get())
invocationCounter.set(0)
assertEquals(null, Subject().lazyProperty)
assertEquals(1, invocationCounter.get())
}
}
@Test
fun `test - lazyProperty - with nullable key - set key to null manually`() {
with(object {
val Subject.lazyProperty by keyANullable.lazyProperty {
fail("Unexpected call to factory function")
}
}) {
val subject = Subject()
subject.extras[keyANullable] = null
assertNull(subject.lazyProperty)
assertTrue(keyANullable in subject.extras)
}
}
}
@@ -24,22 +24,22 @@ class ExtrasTest {
assertNull(extras[extrasKeyOf<String>("a")])
assertNull(extras[extrasKeyOf<String>("b")])
extras[extrasKeyOf()] = "22222"
assertEquals("22222", extras[extrasKeyOf()])
assertNull(extras[extrasKeyOf("a")])
assertNull(extras[extrasKeyOf("b")])
extras[extrasKeyOf<String>()] = "22222"
assertEquals("22222", extras[extrasKeyOf<String>()])
assertNull(extras[extrasKeyOf<String>("a")])
assertNull(extras[extrasKeyOf<String>("b")])
extras[extrasKeyOf("a")] = "value a"
assertEquals("22222", extras[extrasKeyOf()])
assertEquals("value a", extras[extrasKeyOf("a")])
assertEquals("22222", extras[extrasKeyOf<String>()])
assertEquals("value a", extras[extrasKeyOf<String>("a")])
assertNull(extras[extrasKeyOf("b")])
extras[extrasKeyOf("b")] = "value b"
assertEquals("22222", extras[extrasKeyOf()])
assertEquals("value a", extras[extrasKeyOf("a")])
assertEquals("value b", extras[extrasKeyOf("b")])
assertEquals("22222", extras[extrasKeyOf<String>()])
assertEquals("value a", extras[extrasKeyOf<String>("a")])
assertEquals("value b", extras[extrasKeyOf<String>("b")])
assertNull(extras[extrasKeyOf("c")])
assertNull(extras[extrasKeyOf<String>("c")])
}
@Test
@@ -286,4 +286,46 @@ class ExtrasTest {
extras
)
}
@Test
fun `test - extras - nullable key`() {
val empty = extrasOf()
assertNull(empty[extrasKeyOf<String>()])
assertNull(empty[extrasKeyOf<String?>()])
val nonEmpty = extrasOf(
extrasKeyOf<String>() withValue "!",
extrasKeyOf<String?>() withValue "?"
)
assertEquals("!", nonEmpty[extrasKeyOf<String>()])
assertEquals("?", nonEmpty[extrasKeyOf<String?>()])
val containsNull = extrasOf(
extrasKeyOf<String>() withValue "!",
extrasKeyOf<String?>() withValue null
)
assertEquals("!", containsNull[extrasKeyOf<String>()])
assertNull(containsNull[extrasKeyOf<String?>()])
assertTrue(extrasKeyOf<String?>() in containsNull)
}
@Test
fun `test - mutable extras - nullable key`() {
val extras = mutableExtrasOf()
assertNull(extras[extrasKeyOf<String>()])
assertNull(extras[extrasKeyOf<String?>()])
extras[extrasKeyOf<String>()] = "!"
extras[extrasKeyOf<String?>()] = "?"
assertEquals("!", extras[extrasKeyOf<String>()])
assertEquals("?", extras[extrasKeyOf<String?>()])
extras[extrasKeyOf<String?>()] = null
assertEquals("!", extras[extrasKeyOf<String>()])
assertNull(extras[extrasKeyOf<String?>()])
assertTrue(extrasKeyOf<String?>() in extras)
}
}