From 28e7367f7806478de42e3a42aa1d447dd752b0d2 Mon Sep 17 00:00:00 2001 From: Sergey Igushkin Date: Mon, 25 Mar 2024 21:34:03 +0400 Subject: [PATCH] Fix Declarative DSL runtime setting properties in Kotlin types If Kotlin reflection cannot find the property or the setter method, try to look up the setter via Java reflection. Also, refactor some code around `DeclarativeRuntimeProperty`. --- .../DeclarativeReflectionToObjectConverter.kt | 8 +- .../DeclarativeRuntimeProperty.kt | 6 +- .../mappingToJvm/RuntimePropertyResolver.kt | 105 ++++--- ...eDslProjectBuildFileIntegrationSpec.groovy | 273 ++++++++++++------ ...ProjectAccessorsSchemaBuildingComponent.kt | 14 +- 5 files changed, 264 insertions(+), 142 deletions(-) diff --git a/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/DeclarativeReflectionToObjectConverter.kt b/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/DeclarativeReflectionToObjectConverter.kt index ec333481ba9b..eb264ad04cd2 100644 --- a/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/DeclarativeReflectionToObjectConverter.kt +++ b/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/DeclarativeReflectionToObjectConverter.kt @@ -168,8 +168,8 @@ class DeclarativeReflectionToObjectConverter( ?: error("tried to access a property ${dataProperty.name} on a null receiver") val receiverKClass = receiverInstance::class return when (val property = propertyResolver.resolvePropertyRead(receiverKClass, dataProperty.name)) { - is RuntimePropertyResolver.Resolution.Resolved -> property.property.getValue(receiverInstance) - else -> error("cannot get property ${dataProperty.name} from the receiver class $receiverKClass") + is RuntimePropertyResolver.ReadResolution.ResolvedRead -> property.getter.getValue(receiverInstance) + RuntimePropertyResolver.ReadResolution.UnresolvedRead -> error("cannot get property ${dataProperty.name} from the receiver class $receiverKClass") } } @@ -179,8 +179,8 @@ class DeclarativeReflectionToObjectConverter( ?: error("tried to access a property ${dataProperty.name} on a null receiver") val receiverKClass = receiverInstance::class when (val property = propertyResolver.resolvePropertyWrite(receiverKClass, dataProperty.name)) { - is RuntimePropertyResolver.Resolution.Resolved -> property.property.setValue(receiverInstance, value) - RuntimePropertyResolver.Resolution.Unresolved -> error("cannot set property ${dataProperty.name} in the receiver class $receiverKClass") + is RuntimePropertyResolver.WriteResolution.ResolvedWrite -> property.setter.setValue(receiverInstance, value) + RuntimePropertyResolver.WriteResolution.UnresolvedWrite -> error("cannot set property ${dataProperty.name} in the receiver class $receiverKClass") } } } diff --git a/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/DeclarativeRuntimeProperty.kt b/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/DeclarativeRuntimeProperty.kt index 585ff5251cc0..76b2c9752396 100644 --- a/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/DeclarativeRuntimeProperty.kt +++ b/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/DeclarativeRuntimeProperty.kt @@ -17,7 +17,11 @@ package org.gradle.internal.declarativedsl.mappingToJvm -interface DeclarativeRuntimeProperty { +fun interface DeclarativeRuntimePropertyGetter { fun getValue(receiver: Any): Any? +} + + +fun interface DeclarativeRuntimePropertySetter { fun setValue(receiver: Any, value: Any?) } diff --git a/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/RuntimePropertyResolver.kt b/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/RuntimePropertyResolver.kt index 5f229d52bf84..466e1385aa72 100644 --- a/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/RuntimePropertyResolver.kt +++ b/platforms/core-configuration/declarative-dsl-core/src/main/kotlin/org/gradle/internal/declarativedsl/mappingToJvm/RuntimePropertyResolver.kt @@ -16,55 +16,86 @@ package org.gradle.internal.declarativedsl.mappingToJvm -import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver.Resolution.Resolved -import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver.Resolution.Unresolved +import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver.ReadResolution +import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver.ReadResolution.ResolvedRead +import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver.ReadResolution.UnresolvedRead +import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver.WriteResolution +import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver.WriteResolution.ResolvedWrite +import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver.WriteResolution.UnresolvedWrite +import java.lang.reflect.Modifier import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty +import kotlin.reflect.KProperty import kotlin.reflect.KVisibility import kotlin.reflect.full.memberFunctions import kotlin.reflect.full.memberProperties interface RuntimePropertyResolver { - fun resolvePropertyRead(receiverClass: KClass<*>, name: String): Resolution - fun resolvePropertyWrite(receiverClass: KClass<*>, name: String): Resolution + fun resolvePropertyRead(receiverClass: KClass<*>, name: String): ReadResolution + fun resolvePropertyWrite(receiverClass: KClass<*>, name: String): WriteResolution - sealed interface Resolution { - data class Resolved(val property: DeclarativeRuntimeProperty) : Resolution - data object Unresolved : Resolution + sealed interface ReadResolution { + data class ResolvedRead(val getter: DeclarativeRuntimePropertyGetter) : ReadResolution + data object UnresolvedRead : ReadResolution + } + + sealed interface WriteResolution { + data class ResolvedWrite(val setter: DeclarativeRuntimePropertySetter) : WriteResolution + data object UnresolvedWrite : WriteResolution } } object ReflectionRuntimePropertyResolver : RuntimePropertyResolver { - override fun resolvePropertyRead(receiverClass: KClass<*>, name: String): RuntimePropertyResolver.Resolution { - val callable = receiverClass.memberProperties.find { it.name == name && it.visibility == KVisibility.PUBLIC } - ?: receiverClass.memberFunctions.find { it.name == getterName(name) && it.parameters.size == 1 && it.visibility == KVisibility.PUBLIC } - - return when (callable) { - null -> Unresolved - else -> Resolved(object : DeclarativeRuntimeProperty { - override fun getValue(receiver: Any): Any? = callable.call(receiver) - override fun setValue(receiver: Any, value: Any?) = throw UnsupportedOperationException() - }) - } + override fun resolvePropertyRead(receiverClass: KClass<*>, name: String): ReadResolution { + val getter = findKotlinProperty(receiverClass, name)?.let(::kotlinPropertyGetter) + ?: findKotlinFunctionGetter(receiverClass, name) + ?: findJavaGetter(receiverClass, name) + + return getter?.let(::ResolvedRead) ?: UnresolvedRead } - override fun resolvePropertyWrite(receiverClass: KClass<*>, name: String): RuntimePropertyResolver.Resolution { - val setter = (receiverClass.memberProperties.find { it.name == name && it.visibility == KVisibility.PUBLIC } as? KMutableProperty<*>)?.setter - ?: receiverClass.memberFunctions.find { it.name == setterName(name) && it.visibility == KVisibility.PUBLIC } - - return when (setter) { - null -> Unresolved - else -> Resolved(object : DeclarativeRuntimeProperty { - override fun getValue(receiver: Any): Any = throw UnsupportedOperationException() - override fun setValue(receiver: Any, value: Any?) { - setter.call(receiver, value) - } - }) - } + override fun resolvePropertyWrite(receiverClass: KClass<*>, name: String): WriteResolution { + val setter = (findKotlinProperty(receiverClass, name) as? KMutableProperty<*>)?.let(::kotlinPropertySetter) + ?: findKotlinFunctionSetter(receiverClass, name) + ?: findJavaSetter(receiverClass, name) + + return setter?.let(::ResolvedWrite) ?: UnresolvedWrite } + private + fun findKotlinProperty(receiverClass: KClass<*>, name: String) = + receiverClass.memberProperties.find { it.name == name && it.visibility == KVisibility.PUBLIC } + + private + fun kotlinPropertyGetter(property: KProperty<*>) = + DeclarativeRuntimePropertyGetter { property.call(it) } + + private + fun kotlinPropertySetter(property: KMutableProperty<*>) = + DeclarativeRuntimePropertySetter { receiver, value -> property.setter.call(receiver, value) } + + private + fun findKotlinFunctionGetter(receiverClass: KClass<*>, name: String) = + receiverClass.memberFunctions.find { it.name == getterName(name) && it.parameters.size == 1 && it.visibility == KVisibility.PUBLIC } + ?.let { property -> DeclarativeRuntimePropertyGetter { property.call(it) } } + + private + fun findKotlinFunctionSetter(receiverClass: KClass<*>, name: String) = + receiverClass.memberFunctions.find { it.name == setterName(name) && it.visibility == KVisibility.PUBLIC } + ?.let { function -> DeclarativeRuntimePropertySetter { receiver: Any, value: Any? -> function.call(receiver, value) } } + + private + fun findJavaGetter(receiverClass: KClass<*>, name: String) = + receiverClass.java.methods.find { it.name == getterName(name) && it.parameters.isEmpty() && it.modifiers.and(Modifier.PUBLIC) != 0 } + ?.let { method -> DeclarativeRuntimePropertyGetter { method.invoke(it) } } + + private + fun findJavaSetter(receiverClass: KClass<*>, name: String) = + receiverClass.java.methods.find { it.name == setterName(name) && it.parameters.size == 1 && it.modifiers.and(Modifier.PUBLIC) != 0 } + ?.let { method -> DeclarativeRuntimePropertySetter { receiver: Any, value: Any? -> method.invoke(receiver, value) } } + private fun getterName(propertyName: String) = "get" + capitalize(propertyName) @@ -79,21 +110,21 @@ object ReflectionRuntimePropertyResolver : RuntimePropertyResolver { class CompositePropertyResolver(private val resolvers: List) : RuntimePropertyResolver { - override fun resolvePropertyRead(receiverClass: KClass<*>, name: String): RuntimePropertyResolver.Resolution { + override fun resolvePropertyRead(receiverClass: KClass<*>, name: String): ReadResolution { resolvers.forEach { val resolution = it.resolvePropertyRead(receiverClass, name) - if (resolution is Resolved) + if (resolution !is UnresolvedRead) return resolution } - return Unresolved + return UnresolvedRead } - override fun resolvePropertyWrite(receiverClass: KClass<*>, name: String): RuntimePropertyResolver.Resolution { + override fun resolvePropertyWrite(receiverClass: KClass<*>, name: String): WriteResolution { resolvers.forEach { val resolution = it.resolvePropertyWrite(receiverClass, name) - if (resolution is Resolved) + if (resolution !is UnresolvedWrite) return resolution } - return Unresolved + return UnresolvedWrite } } diff --git a/platforms/core-configuration/declarative-dsl-provider/src/integTest/groovy/org/gradle/internal/declarativedsl/project/DeclarativeDslProjectBuildFileIntegrationSpec.groovy b/platforms/core-configuration/declarative-dsl-provider/src/integTest/groovy/org/gradle/internal/declarativedsl/project/DeclarativeDslProjectBuildFileIntegrationSpec.groovy index 238654d882fe..a5afdc17c8c2 100644 --- a/platforms/core-configuration/declarative-dsl-provider/src/integTest/groovy/org/gradle/internal/declarativedsl/project/DeclarativeDslProjectBuildFileIntegrationSpec.groovy +++ b/platforms/core-configuration/declarative-dsl-provider/src/integTest/groovy/org/gradle/internal/declarativedsl/project/DeclarativeDslProjectBuildFileIntegrationSpec.groovy @@ -18,6 +18,7 @@ package org.gradle.internal.declarativedsl.project import org.gradle.integtests.fixtures.AbstractIntegrationSpec +import org.intellij.lang.annotations.Language class DeclarativeDslProjectBuildFileIntegrationSpec extends AbstractIntegrationSpec { @@ -105,12 +106,14 @@ class DeclarativeDslProjectBuildFileIntegrationSpec extends AbstractIntegrationS ].every { it.isFile() && it.text != "" } } - def 'can configure a custom plugin extension in declarative DSL'() { + def 'can configure a custom plugin extension in declarative DSL for a plugin written in #language'() { given: - file("buildSrc/build.gradle") << """ + file("buildSrc/build.gradle.kts") << """ plugins { - id('java-gradle-plugin') + `java-gradle-plugin` + ${if (language == "kotlin") { "`kotlin-dsl`" } else { "" }} } + ${if (language == "kotlin") { "repositories { mavenCentral() }" } else { "" }} gradlePlugin { plugins { create("restrictedPlugin") { @@ -121,92 +124,7 @@ class DeclarativeDslProjectBuildFileIntegrationSpec extends AbstractIntegrationS } """ - file("buildSrc/src/main/java/com/example/restricted/Extension.java") << """ - package com.example.restricted; - - import org.gradle.declarative.dsl.model.annotations.Adding; - import org.gradle.declarative.dsl.model.annotations.Configuring; - import org.gradle.declarative.dsl.model.annotations.Restricted; - import org.gradle.api.Action; - import org.gradle.api.model.ObjectFactory; - import org.gradle.api.provider.ListProperty; - import org.gradle.api.provider.Property; - - import javax.inject.Inject; - - @Restricted - public abstract class Extension { - private final Access primaryAccess; - public abstract ListProperty getSecondaryAccess(); - private final ObjectFactory objects; - - public Access getPrimaryAccess() { - return primaryAccess; - } - - @Inject - public Extension(ObjectFactory objects) { - this.objects = objects; - this.primaryAccess = objects.newInstance(Access.class); - this.primaryAccess.getName().set("primary"); - - getId().convention(""); - getReferencePoint().convention(point(-1, -1)); - } - - @Restricted - public abstract Property getId(); - - @Restricted - public abstract Property getReferencePoint(); - - @Configuring - public void primaryAccess(Action configure) { - configure.execute(primaryAccess); - } - - @Adding - public Access secondaryAccess(Action configure) { - Access newAccess = objects.newInstance(Access.class); - newAccess.getName().convention(""); - configure.execute(newAccess); - getSecondaryAccess().add(newAccess); - return newAccess; - } - - @Restricted - public Point point(int x, int y) { - return new Point(x, y); - } - - public abstract static class Access { - public Access() { - getName().convention(""); - getRead().convention(false); - getWrite().convention(false); - } - - @Restricted - public abstract Property getName(); - - @Restricted - public abstract Property getRead(); - - @Restricted - public abstract Property getWrite(); - } - - public static class Point { - public final int x; - public final int y; - - public Point(int x, int y) { - this.x = x; - this.y = y; - } - } - } - """ + file(extensionFile) << extensionCode file("buildSrc/src/main/java/com/example/restricted/RestrictedPlugin.java") << """ package com.example.restricted; @@ -230,7 +148,7 @@ class DeclarativeDslProjectBuildFileIntegrationSpec extends AbstractIntegrationS task.doLast("print restricted extension content", t -> { System.out.println("id = " + restricted.getId().get()); Extension.Point point = referencePoint.getOrElse(restricted.point(-1, -1)); - System.out.println("referencePoint = (" + point.x + ", " + point.y + ")"); + System.out.println("referencePoint = (" + point.getX() + ", " + point.getY() + ")"); System.out.println("primaryAccess = { " + acc.getName().get() + ", " + acc.getRead().get() + ", " + acc.getWrite().get() + "}" ); @@ -285,8 +203,183 @@ primaryAccess = { primary, false, false} secondaryAccess { two, true, false} secondaryAccess { three, true, true}""" ) + + where: + language | extensionFile | extensionCode + "java" | JAVA_PLUGIN_EXTENSION_FILENAME | JAVA_PLUGIN_EXTENSION + "kotlin" | KOTLIN_PLUGIN_EXTENSION_FILENAME | KOTLIN_PLUGIN_EXTENSION } + private static final JAVA_PLUGIN_EXTENSION_FILENAME = "buildSrc/src/main/java/com/example/restricted/Extension.java" + private static final KOTLIN_PLUGIN_EXTENSION_FILENAME = "buildSrc/src/main/kotlin/com/example/restricted/Extension.kt" + + @Language("java") + private static final JAVA_PLUGIN_EXTENSION = """ + package com.example.restricted; + + import org.gradle.declarative.dsl.model.annotations.Adding; + import org.gradle.declarative.dsl.model.annotations.Configuring; + import org.gradle.declarative.dsl.model.annotations.Restricted; + import org.gradle.api.Action; + import org.gradle.api.model.ObjectFactory; + import org.gradle.api.provider.ListProperty; + import org.gradle.api.provider.Property; + + import javax.inject.Inject; + + @Restricted + public abstract class Extension { + private final Access primaryAccess; + public abstract ListProperty getSecondaryAccess(); + private final ObjectFactory objects; + + public Access getPrimaryAccess() { + return primaryAccess; + } + + @Inject + public Extension(ObjectFactory objects) { + this.objects = objects; + this.primaryAccess = objects.newInstance(Access.class); + this.primaryAccess.getName().set("primary"); + + getId().convention(""); + getReferencePoint().convention(point(-1, -1)); + } + + @Restricted + public abstract Property getId(); + + @Restricted + public abstract Property getReferencePoint(); + + @Configuring + public void primaryAccess(Action configure) { + configure.execute(primaryAccess); + } + + @Adding + public Access secondaryAccess(Action configure) { + Access newAccess = objects.newInstance(Access.class); + newAccess.getName().convention(""); + configure.execute(newAccess); + getSecondaryAccess().add(newAccess); + return newAccess; + } + + @Restricted + public Point point(int x, int y) { + return new Point(x, y); + } + + public abstract static class Access { + public Access() { + getName().convention(""); + getRead().convention(false); + getWrite().convention(false); + } + + @Restricted + public abstract Property getName(); + + @Restricted + public abstract Property getRead(); + + @Restricted + public abstract Property getWrite(); + } + + public static class Point { + public final int xCoord; + public final int yCoord; + + public Point(int x, int y) { + this.xCoord = x; + this.yCoord = y; + } + + public int getX() { + return xCoord; + } + + public int getY() { + return yCoord; + } + } + } + """.stripIndent() + + @Language("kotlin") + private static final KOTLIN_PLUGIN_EXTENSION = """ + package com.example.restricted + + import org.gradle.api.model.ObjectFactory + import org.gradle.api.provider.ListProperty + import org.gradle.api.provider.Property + import org.gradle.declarative.dsl.model.annotations.Adding + import org.gradle.declarative.dsl.model.annotations.Configuring + import org.gradle.declarative.dsl.model.annotations.Restricted + import javax.inject.Inject + + @Restricted + abstract class Extension @Inject constructor(private val objects: ObjectFactory) { + val primaryAccess: Access + abstract val secondaryAccess: ListProperty + + init { + this.primaryAccess = objects.newInstance(Access::class.java) + primaryAccess.name.set("primary") + + id.convention("") + referencePoint.convention(point(-1, -1)) + } + + @get:Restricted + abstract val id: Property + + @get:Restricted + abstract val referencePoint: Property + + @Configuring + fun primaryAccess(configure: Access.() -> Unit) { + configure(primaryAccess) + } + + @Adding + fun secondaryAccess(configure: Access.() -> Unit): Access { + val newAccess = objects.newInstance(Access::class.java) + newAccess.name.convention("") + configure(newAccess) + secondaryAccess.add(newAccess) + return newAccess + } + + @Restricted + fun point(x: Int, y: Int): Point { + return Point(x, y) + } + + abstract class Access { + init { + name.convention("") + read.convention(false) + write.convention(false) + } + + @get:Restricted + abstract val name: Property + + @get:Restricted + abstract val read: Property + + @get:Restricted + abstract val write: Property + } + + class Point(val x: Int, val y: Int) + } + """ + def 'reports #kind errors in project file #part'() { given: file("build.gradle.something") << """ diff --git a/platforms/core-configuration/declarative-dsl-provider/src/main/kotlin/org/gradle/internal/declarativedsl/project/TypeSafeProjectAccessorsSchemaBuildingComponent.kt b/platforms/core-configuration/declarative-dsl-provider/src/main/kotlin/org/gradle/internal/declarativedsl/project/TypeSafeProjectAccessorsSchemaBuildingComponent.kt index ab6401440ea2..3b4da4dd49cd 100644 --- a/platforms/core-configuration/declarative-dsl-provider/src/main/kotlin/org/gradle/internal/declarativedsl/project/TypeSafeProjectAccessorsSchemaBuildingComponent.kt +++ b/platforms/core-configuration/declarative-dsl-provider/src/main/kotlin/org/gradle/internal/declarativedsl/project/TypeSafeProjectAccessorsSchemaBuildingComponent.kt @@ -22,7 +22,6 @@ import org.gradle.internal.declarativedsl.analysis.DataProperty import org.gradle.internal.declarativedsl.analysis.DataTypeRef import org.gradle.internal.declarativedsl.analysis.FqName import org.gradle.internal.declarativedsl.evaluationSchema.EvaluationSchemaComponent -import org.gradle.internal.declarativedsl.mappingToJvm.DeclarativeRuntimeProperty import org.gradle.internal.declarativedsl.mappingToJvm.RuntimePropertyResolver import org.gradle.internal.declarativedsl.schemaBuilder.CollectedPropertyInformation import org.gradle.internal.declarativedsl.schemaBuilder.DefaultPropertyExtractor @@ -87,17 +86,12 @@ class TypesafeProjectAccessorsComponent(targetScope: ClassLoaderScope) : Evaluat private class ProjectPropertyAccessorRuntimeResolver : RuntimePropertyResolver { - override fun resolvePropertyRead(receiverClass: KClass<*>, name: String): RuntimePropertyResolver.Resolution { + override fun resolvePropertyRead(receiverClass: KClass<*>, name: String): RuntimePropertyResolver.ReadResolution = if (receiverClass.isSubclassOf(Project::class) && name == "projects") { - return RuntimePropertyResolver.Resolution.Resolved(object : DeclarativeRuntimeProperty { - override fun getValue(receiver: Any) = (receiver as Project).extensions.getByName("projects") - override fun setValue(receiver: Any, value: Any?): Unit = throw UnsupportedOperationException() - }) - } - return RuntimePropertyResolver.Resolution.Unresolved - } + RuntimePropertyResolver.ReadResolution.ResolvedRead { receiver -> (receiver as Project).extensions.getByName("projects") } + } else RuntimePropertyResolver.ReadResolution.UnresolvedRead - override fun resolvePropertyWrite(receiverClass: KClass<*>, name: String): RuntimePropertyResolver.Resolution = RuntimePropertyResolver.Resolution.Unresolved + override fun resolvePropertyWrite(receiverClass: KClass<*>, name: String) = RuntimePropertyResolver.WriteResolution.UnresolvedWrite }