[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.loadProperty
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.presetName
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_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_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_SLOW_SOURCES_JAR_RESOLVER = property("kotlin.mpp.import.enableSlowSourcesJarResolver")
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_NATIVE_USE_XCODE_MESSAGE_STYLE = property("kotlin.native.useXcodeMessageStyle")
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_RUN_COMPILER_VIA_BUILD_TOOLS_API = property("kotlin.compiler.runViaBuildToolsApi")
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_SUPPRESS_EXPERIMENTAL_ARTIFACTS_DSL_WARNING = property("kotlin.native.suppressExperimentalArtifactsDslWarning")
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
* See [org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.InternalGradlePropertiesUsageChecker]
**/
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_SHOW_STACKTRACE = property("$KOTLIN_INTERNAL_NAMESPACE.diagnostics.showStacktrace")
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.lowerCamelCaseName
import org.jetbrains.kotlin.gradle.utils.markResolvable
import org.jetbrains.kotlin.tooling.core.extrasLazyProperty
/**
* @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
* consistently across the whole project.
*/
internal val InternalKotlinSourceSet.resolvableMetadataConfiguration: Configuration by extrasLazyProperty(
"resolvableMetadataConfiguration"
) {
internal val InternalKotlinSourceSet.resolvableMetadataConfiguration: Configuration by storedExtrasProperty {
assert(resolvableMetadataConfigurationName !in project.configurations.names)
val configuration = project.configurations.maybeCreate(resolvableMetadataConfigurationName)
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
relies on dependencies being added explicitly by the Kotlin Gradle Plugin
*/
*/
private fun InternalKotlinSourceSet.configureMetadataDependenciesConfigurations(resolvableMetadataConfiguration: Configuration) {
@Suppress("DEPRECATION")
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)
}
}