diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt index 10c57bc0524..0684bf44a0b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt @@ -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") diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resolvableMetadataConfiguration.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resolvableMetadataConfiguration.kt index 8339c12b9db..683a7fbca92 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resolvableMetadataConfiguration.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resolvableMetadataConfiguration.kt @@ -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( diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/storedProperty.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/storedProperty.kt new file mode 100644 index 00000000000..75d61c81e21 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/storedProperty.kt @@ -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 { + * fun apply(project: Project) { + * // prints 'Foo("a")' + * println(project.myFoo) + * } + * } + * ``` + * _Usage in Project 'b'_ + * + * ```kotlin + * class MyPlugin : Plugin { + * 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 storedProjectProperty(initializer: Project.() -> T): ReadOnlyProperty = StoredLazyProperty( + storage = { storedPropertyStorage }, + initializer = initializer +) + +/** + * Same as [storedProjectProperty], but will allow storing the property on any object implementing [HasMutableExtras] + */ +internal fun storedExtrasProperty(initializer: R.() -> T): ReadOnlyProperty = StoredLazyProperty( + storage = { storedPropertyStorage }, + initializer = initializer +) + + +private interface StoredProperty + +private class StoredLazyProperty( + private val storage: T.() -> StoredPropertyStorage, + private val initializer: T.() -> V, +) : ReadOnlyProperty, StoredProperty { + 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, Any?>() + fun getOrPut(key: StoredLazyProperty<*, T>, create: () -> T): T { + @Suppress("UNCHECKED_CAST") + return values.getOrPut(key) { create() } as T + } + + @Suppress("UNCHECKED_CAST") + operator fun get(key: StoredProperty): T? { + return values[key] as T? + } +} + +private val Project.storedPropertyStorage + get() = extraProperties.getOrPut(StoredPropertyStorage::class.java.name) { StoredPropertyStorage() } + +private val storedPropertyStorageExtrasKey = extrasKeyOf() + +private val HasMutableExtras.storedPropertyStorage + get() = extras.getOrPut(storedPropertyStorageExtrasKey) { StoredPropertyStorage() } diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/StoredPropertyTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/StoredPropertyTest.kt new file mode 100644 index 00000000000..d3c8313a056 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/StoredPropertyTest.kt @@ -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) + } +}