[Gradle] Implement generic 'StoredProperty' mechanism

This will allow extending objects of 'Project' or 'HasMutableExtras'
with generic, stored instances

KT-61634
This commit is contained in:
Sebastian Sellmair
2023-09-01 14:11:25 +02:00
committed by Space Team
parent 1fd9706f47
commit e78331cd5c
4 changed files with 223 additions and 9 deletions
@@ -57,6 +57,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
import org.jetbrains.kotlin.gradle.utils.NativeCompilerDownloader import org.jetbrains.kotlin.gradle.utils.NativeCompilerDownloader
import org.jetbrains.kotlin.gradle.utils.loadProperty import org.jetbrains.kotlin.gradle.utils.loadProperty
import org.jetbrains.kotlin.gradle.utils.localProperties import org.jetbrains.kotlin.gradle.utils.localProperties
import org.jetbrains.kotlin.gradle.utils.storedProjectProperty
import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.target.presetName import org.jetbrains.kotlin.konan.target.presetName
import org.jetbrains.kotlin.statistics.metrics.StringMetrics import org.jetbrains.kotlin.statistics.metrics.StringMetrics
@@ -606,7 +607,8 @@ internal class PropertiesProvider private constructor(private val project: Proje
val KOTLIN_MPP_HIERARCHICAL_STRUCTURE_SUPPORT = property("kotlin.mpp.hierarchicalStructureSupport") val KOTLIN_MPP_HIERARCHICAL_STRUCTURE_SUPPORT = property("kotlin.mpp.hierarchicalStructureSupport")
val KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN = property("kotlin.mpp.androidGradlePluginCompatibility.nowarn") val KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN = property("kotlin.mpp.androidGradlePluginCompatibility.nowarn")
val KOTLIN_MPP_ANDROID_SOURCE_SET_LAYOUT_VERSION = property("kotlin.mpp.androidSourceSetLayoutVersion") val KOTLIN_MPP_ANDROID_SOURCE_SET_LAYOUT_VERSION = property("kotlin.mpp.androidSourceSetLayoutVersion")
val KOTLIN_MPP_ANDROID_SOURCE_SET_LAYOUT_ANDROID_STYLE_NO_WARN = property("kotlin.mpp.androidSourceSetLayoutV2AndroidStyleDirs.nowarn") val KOTLIN_MPP_ANDROID_SOURCE_SET_LAYOUT_ANDROID_STYLE_NO_WARN =
property("kotlin.mpp.androidSourceSetLayoutV2AndroidStyleDirs.nowarn")
val KOTLIN_MPP_IMPORT_ENABLE_KGP_DEPENDENCY_RESOLUTION = property("kotlin.mpp.import.enableKgpDependencyResolution") val KOTLIN_MPP_IMPORT_ENABLE_KGP_DEPENDENCY_RESOLUTION = property("kotlin.mpp.import.enableKgpDependencyResolution")
val KOTLIN_MPP_IMPORT_ENABLE_SLOW_SOURCES_JAR_RESOLVER = property("kotlin.mpp.import.enableSlowSourcesJarResolver") val KOTLIN_MPP_IMPORT_ENABLE_SLOW_SOURCES_JAR_RESOLVER = property("kotlin.mpp.import.enableSlowSourcesJarResolver")
val KOTLIN_MPP_ENABLE_INTRANSITIVE_METADATA_CONFIGURATION = property("kotlin.mpp.enableIntransitiveMetadataConfiguration") val KOTLIN_MPP_ENABLE_INTRANSITIVE_METADATA_CONFIGURATION = property("kotlin.mpp.enableIntransitiveMetadataConfiguration")
@@ -624,7 +626,8 @@ internal class PropertiesProvider private constructor(private val project: Proje
val KOTLIN_OPTIONS_SUPPRESS_FREEARGS_MODIFICATION_WARNING = property("kotlin.options.suppressFreeCompilerArgsModificationWarning") val KOTLIN_OPTIONS_SUPPRESS_FREEARGS_MODIFICATION_WARNING = property("kotlin.options.suppressFreeCompilerArgsModificationWarning")
val KOTLIN_NATIVE_USE_XCODE_MESSAGE_STYLE = property("kotlin.native.useXcodeMessageStyle") val KOTLIN_NATIVE_USE_XCODE_MESSAGE_STYLE = property("kotlin.native.useXcodeMessageStyle")
val KOTLIN_COMPILER_USE_PRECISE_COMPILATION_RESULTS_BACKUP = property("kotlin.compiler.preciseCompilationResultsBackup") val KOTLIN_COMPILER_USE_PRECISE_COMPILATION_RESULTS_BACKUP = property("kotlin.compiler.preciseCompilationResultsBackup")
val KOTLIN_COMPILER_KEEP_INCREMENTAL_COMPILATION_CACHES_IN_MEMORY = property("kotlin.compiler.keepIncrementalCompilationCachesInMemory") val KOTLIN_COMPILER_KEEP_INCREMENTAL_COMPILATION_CACHES_IN_MEMORY =
property("kotlin.compiler.keepIncrementalCompilationCachesInMemory")
val KOTLIN_SUPPRESS_EXPERIMENTAL_IC_OPTIMIZATIONS_WARNING = property("kotlin.compiler.suppressExperimentalICOptimizationsWarning") val KOTLIN_SUPPRESS_EXPERIMENTAL_IC_OPTIMIZATIONS_WARNING = property("kotlin.compiler.suppressExperimentalICOptimizationsWarning")
val KOTLIN_RUN_COMPILER_VIA_BUILD_TOOLS_API = property("kotlin.compiler.runViaBuildToolsApi") val KOTLIN_RUN_COMPILER_VIA_BUILD_TOOLS_API = property("kotlin.compiler.runViaBuildToolsApi")
val KOTLIN_MPP_ALLOW_LEGACY_DEPENDENCIES = property("kotlin.mpp.allow.legacy.dependencies") val KOTLIN_MPP_ALLOW_LEGACY_DEPENDENCIES = property("kotlin.mpp.allow.legacy.dependencies")
@@ -634,14 +637,16 @@ internal class PropertiesProvider private constructor(private val project: Proje
val KOTLIN_NATIVE_IGNORE_DISABLED_TARGETS = property("kotlin.native.ignoreDisabledTargets") val KOTLIN_NATIVE_IGNORE_DISABLED_TARGETS = property("kotlin.native.ignoreDisabledTargets")
val KOTLIN_NATIVE_SUPPRESS_EXPERIMENTAL_ARTIFACTS_DSL_WARNING = property("kotlin.native.suppressExperimentalArtifactsDslWarning") val KOTLIN_NATIVE_SUPPRESS_EXPERIMENTAL_ARTIFACTS_DSL_WARNING = property("kotlin.native.suppressExperimentalArtifactsDslWarning")
val KONAN_DATA_DIR = property("konan.data.dir") val KONAN_DATA_DIR = property("konan.data.dir")
val KOTLIN_SUPPRESS_BUILD_TOOLS_API_VERSION_CONSISTENCY_CHECKS = property("kotlin.internal.suppress.buildToolsApiVersionConsistencyChecks") val KOTLIN_SUPPRESS_BUILD_TOOLS_API_VERSION_CONSISTENCY_CHECKS =
property("kotlin.internal.suppress.buildToolsApiVersionConsistencyChecks")
/** /**
* Internal properties: builds get big non-suppressible warning when such properties are used * Internal properties: builds get big non-suppressible warning when such properties are used
* See [org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.InternalGradlePropertiesUsageChecker] * See [org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.InternalGradlePropertiesUsageChecker]
**/ **/
val KOTLIN_MPP_HIERARCHICAL_STRUCTURE_BY_DEFAULT = property("$KOTLIN_INTERNAL_NAMESPACE.mpp.hierarchicalStructureByDefault") val KOTLIN_MPP_HIERARCHICAL_STRUCTURE_BY_DEFAULT = property("$KOTLIN_INTERNAL_NAMESPACE.mpp.hierarchicalStructureByDefault")
val KOTLIN_CREATE_DEFAULT_MULTIPLATFORM_PUBLICATIONS = property("$KOTLIN_INTERNAL_NAMESPACE.mpp.createDefaultMultiplatformPublications") val KOTLIN_CREATE_DEFAULT_MULTIPLATFORM_PUBLICATIONS =
property("$KOTLIN_INTERNAL_NAMESPACE.mpp.createDefaultMultiplatformPublications")
val KOTLIN_INTERNAL_DIAGNOSTICS_USE_PARSABLE_FORMATTING = property("$KOTLIN_INTERNAL_NAMESPACE.diagnostics.useParsableFormatting") val KOTLIN_INTERNAL_DIAGNOSTICS_USE_PARSABLE_FORMATTING = property("$KOTLIN_INTERNAL_NAMESPACE.diagnostics.useParsableFormatting")
val KOTLIN_INTERNAL_DIAGNOSTICS_SHOW_STACKTRACE = property("$KOTLIN_INTERNAL_NAMESPACE.diagnostics.showStacktrace") val KOTLIN_INTERNAL_DIAGNOSTICS_SHOW_STACKTRACE = property("$KOTLIN_INTERNAL_NAMESPACE.diagnostics.showStacktrace")
val KOTLIN_SUPPRESS_GRADLE_PLUGIN_ERRORS = property("$KOTLIN_INTERNAL_NAMESPACE.suppressGradlePluginErrors") val KOTLIN_SUPPRESS_GRADLE_PLUGIN_ERRORS = property("$KOTLIN_INTERNAL_NAMESPACE.suppressGradlePluginErrors")
@@ -23,7 +23,6 @@ import org.jetbrains.kotlin.gradle.utils.getOrCreate
import org.jetbrains.kotlin.gradle.utils.listProperty import org.jetbrains.kotlin.gradle.utils.listProperty
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.gradle.utils.markResolvable import org.jetbrains.kotlin.gradle.utils.markResolvable
import org.jetbrains.kotlin.tooling.core.extrasLazyProperty
/** /**
* @see resolvableMetadataConfiguration * @see resolvableMetadataConfiguration
@@ -36,9 +35,7 @@ internal val InternalKotlinSourceSet.resolvableMetadataConfigurationName: String
* These dependencies are set up to resolve Kotlin Metadata (without transformation) and will resolve * These dependencies are set up to resolve Kotlin Metadata (without transformation) and will resolve
* consistently across the whole project. * consistently across the whole project.
*/ */
internal val InternalKotlinSourceSet.resolvableMetadataConfiguration: Configuration by extrasLazyProperty( internal val InternalKotlinSourceSet.resolvableMetadataConfiguration: Configuration by storedExtrasProperty {
"resolvableMetadataConfiguration"
) {
assert(resolvableMetadataConfigurationName !in project.configurations.names) assert(resolvableMetadataConfigurationName !in project.configurations.names)
val configuration = project.configurations.maybeCreate(resolvableMetadataConfigurationName) val configuration = project.configurations.maybeCreate(resolvableMetadataConfigurationName)
configuration.markResolvable() configuration.markResolvable()
@@ -79,7 +76,7 @@ Dependencies will be coming from extending the newer 'resolvableMetadataConfigur
the intransitiveMetadataConfigurationName will not extend this mechanism, since it only the intransitiveMetadataConfigurationName will not extend this mechanism, since it only
relies on dependencies being added explicitly by the Kotlin Gradle Plugin relies on dependencies being added explicitly by the Kotlin Gradle Plugin
*/ */
private fun InternalKotlinSourceSet.configureMetadataDependenciesConfigurations(resolvableMetadataConfiguration: Configuration) { private fun InternalKotlinSourceSet.configureMetadataDependenciesConfigurations(resolvableMetadataConfiguration: Configuration) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
listOf( listOf(
@@ -0,0 +1,123 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.gradle.utils
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.extraProperties
import org.jetbrains.kotlin.tooling.core.HasMutableExtras
import org.jetbrains.kotlin.tooling.core.extrasKeyOf
import org.jetbrains.kotlin.tooling.core.getOrPut
import java.util.*
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* ### Generic mechanism of attaching 'data' to [Project]
* #### e.g. attaching a simple property to a [Project]
* ```kotlin
* class Foo(val projectName: String)
*
* val Project.myFoo by storedProjectProperty {
* Foo(project.name)
* }
* ```
*
* _Usage in Project 'a'_
*
* ```kotlin
* class MyPlugin : Plugin<Project> {
* fun apply(project: Project) {
* // prints 'Foo("a")'
* println(project.myFoo)
* }
* }
* ```
* _Usage in Project 'b'_
*
* ```kotlin
* class MyPlugin : Plugin<Project> {
* fun apply(project: Project) {
* // prints 'Foo("b")'
* println(project.myFoo)
* }
* }
* ```
*
* ### Note:
* The key used for storing the property to the [Project] is the instance of the returned [ReadOnlyProperty],
* *not* the type, or any String based key
*
*/
internal fun <T : Any> storedProjectProperty(initializer: Project.() -> T): ReadOnlyProperty<Project, T> = StoredLazyProperty(
storage = { storedPropertyStorage },
initializer = initializer
)
/**
* Same as [storedProjectProperty], but will allow storing the property on any object implementing [HasMutableExtras]
*/
internal fun <R : HasMutableExtras, T : Any> storedExtrasProperty(initializer: R.() -> T): ReadOnlyProperty<R, T> = StoredLazyProperty(
storage = { storedPropertyStorage },
initializer = initializer
)
private interface StoredProperty<out T>
private class StoredLazyProperty<in T, out V>(
private val storage: T.() -> StoredPropertyStorage,
private val initializer: T.() -> V,
) : ReadOnlyProperty<T, V>, StoredProperty<V> {
override fun getValue(thisRef: T, property: KProperty<*>): V {
return thisRef.storage().getOrPut(this) { thisRef.initializer() }
}
}
private class StoredPropertyStorage {
/**
* The stored value of the [StoredProperty] will be kept as long as the reference to the [StoredProperty] is alive.
* e.g. a top level delegate like
* ```kotlin
* val Project.myProperty by storedProjectProperty { 42 }
* ```
*
* will be effectively kept forever as the [StoredProperty] is referenced globally from the static scope
* of the containing Source File.
*
* However:
*
* ```kotlin
* class MyClass {
* val Project.myProperty by storedProjectProperty { 42 }
* }
*
* val myInstance = MyClass()
* ```
*
* Such code would keep the property referenced from `myInstance`. Once this `myInstance` is collected,
* there is no need for keeping the attached/stored property. `myInstance` getting collected leads to `myPropert$storedProjectProperty`
* getting collected and the associated value will be released
*/
private val values = WeakHashMap<StoredProperty<*>, Any?>()
fun <T> getOrPut(key: StoredLazyProperty<*, T>, create: () -> T): T {
@Suppress("UNCHECKED_CAST")
return values.getOrPut(key) { create() } as T
}
@Suppress("UNCHECKED_CAST")
operator fun <T> get(key: StoredProperty<T>): T? {
return values[key] as T?
}
}
private val Project.storedPropertyStorage
get() = extraProperties.getOrPut(StoredPropertyStorage::class.java.name) { StoredPropertyStorage() }
private val storedPropertyStorageExtrasKey = extrasKeyOf<StoredPropertyStorage>()
private val HasMutableExtras.storedPropertyStorage
get() = extras.getOrPut(storedPropertyStorageExtrasKey) { StoredPropertyStorage() }
@@ -0,0 +1,89 @@
/*
* Copyright 2010-2023 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("FunctionName")
package org.jetbrains.kotlin.gradle.unitTests
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.util.buildProject
import org.jetbrains.kotlin.gradle.utils.storedExtrasProperty
import org.jetbrains.kotlin.gradle.utils.storedProjectProperty
import org.jetbrains.kotlin.tooling.core.HasMutableExtras
import org.jetbrains.kotlin.tooling.core.MutableExtras
import org.jetbrains.kotlin.tooling.core.mutableExtrasOf
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertSame
class StoredPropertyTest {
private data class Value(val id: Any)
private val Project.projectNameProperty by storedProjectProperty { Value(project.name) }
private val atomic = AtomicInteger(0)
private val Project.counterProperty by storedProjectProperty { Value(atomic.getAndIncrement()) }
private class Scope(val id: Any) {
val Project.myProperty by storedProjectProperty { Value(id) }
}
private class TestEntity(val id: Any) : HasMutableExtras {
override val extras: MutableExtras = mutableExtrasOf()
}
private val TestEntity.myProperty by storedExtrasProperty { Value(id) }
@Test
fun `test - projectNameProperty`() {
val projectA = buildProject({ withName("a") })
val projectB = buildProject({ withName("b") })
assertEquals(Value("a"), projectA.projectNameProperty)
assertEquals(Value("b"), projectB.projectNameProperty)
assertSame(projectA.projectNameProperty, projectA.projectNameProperty)
assertSame(projectB.projectNameProperty, projectB.projectNameProperty)
}
@Test
fun `test - counterProperty`() {
val projectA = buildProject()
val projectB = buildProject()
assertEquals(Value(0), projectA.counterProperty)
assertEquals(Value(0), projectA.counterProperty)
assertEquals(Value(1), projectB.counterProperty)
assertEquals(Value(1), projectB.counterProperty)
}
@Test
fun `test - Scope`() {
val project = buildProject()
val scopeA = Scope("a")
val scopeB = Scope("b")
with(scopeA) {
assertEquals(Value("a"), project.myProperty)
}
with(scopeB) {
assertEquals(Value("b"), project.myProperty)
}
}
@Test
fun `test - TestEntity - myProperty`() {
val entityA = TestEntity("a")
val entityB = TestEntity("b")
assertEquals(Value("a"), entityA.myProperty)
assertEquals(Value("b"), entityB.myProperty)
assertSame(entityA.myProperty, entityA.myProperty)
assertSame(entityB.myProperty, entityB.myProperty)
}
}