From d510c9324181666062f7b4d7797723fc593c88f6 Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Thu, 3 Aug 2023 13:10:26 +0200 Subject: [PATCH] [Commonizer] Support generic/simple annotation commonization (to support kotlinx.cinterop.* annotations) ^KT-59302 Verification Pending --- .../src/commonMain/kotlin/Common.kt | 1 + .../src/upperMain/kotlin/SharedNative.kt | 1 + .../src/linuxArm64Main/kotlin/LinuxArm64.kt | 1 + .../src/linuxMain/kotlin/LinuxMain.kt | 1 + .../src/linuxX64Main/kotlin/LinuxX64.kt | 1 + .../src/linuxArm64Main/kotlin/LinuxArm64.kt | 1 + .../lib/src/linuxMain/kotlin/LinuxMain.kt | 1 + .../lib/src/linuxX64Main/kotlin/LinuxX64.kt | 1 + .../commonizer/core/AnnotationsCommonizer.kt | 275 +++--------------- .../core/ClassConstructorCommonizer.kt | 2 +- .../core/DeprecationAnnotationCommonizer.kt | 162 +++++++++++ .../commonizer/core/FunctionCommonizer.kt | 2 +- .../core/AnnotationsCommonizerTest.kt | 8 +- .../HierarchicalFunctionCommonizationTest.kt | 33 +++ 14 files changed, 245 insertions(+), 245 deletions(-) create mode 100644 native/commonizer/src/org/jetbrains/kotlin/commonizer/core/DeprecationAnnotationCommonizer.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/commonize-kt-48118-c-interop-in-common-main/src/commonMain/kotlin/Common.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/commonize-kt-48118-c-interop-in-common-main/src/commonMain/kotlin/Common.kt index b97df657174..70ab9c79581 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/commonize-kt-48118-c-interop-in-common-main/src/commonMain/kotlin/Common.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/commonize-kt-48118-c-interop-in-common-main/src/commonMain/kotlin/Common.kt @@ -1,3 +1,4 @@ fun c(){ + @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) dummy.max(2,3) } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/commonize-kt-48118-c-interop-in-common-main/src/upperMain/kotlin/SharedNative.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/commonize-kt-48118-c-interop-in-common-main/src/upperMain/kotlin/SharedNative.kt index 5bf4e8b18a4..c9c4f8ab032 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/commonize-kt-48118-c-interop-in-common-main/src/upperMain/kotlin/SharedNative.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/commonize-kt-48118-c-interop-in-common-main/src/upperMain/kotlin/SharedNative.kt @@ -1,3 +1,4 @@ fun sn(){ + @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) dummy.max(1,2) } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxArm64Main/kotlin/LinuxArm64.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxArm64Main/kotlin/LinuxArm64.kt index d5c90bf91b4..8effd2a9ffa 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxArm64Main/kotlin/LinuxArm64.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxArm64Main/kotlin/LinuxArm64.kt @@ -1,5 +1,6 @@ package includedLib +@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun linuxArm64( a: cinterop.a.StructA ) { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxMain/kotlin/LinuxMain.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxMain/kotlin/LinuxMain.kt index 54d99c5561c..0ecb667cbe1 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxMain/kotlin/LinuxMain.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxMain/kotlin/LinuxMain.kt @@ -4,6 +4,7 @@ import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.alloc import kotlinx.cinterop.memScoped +@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun linuxMain() { cinterop.a.a() @OptIn(ExperimentalForeignApi::class) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxX64Main/kotlin/LinuxX64.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxX64Main/kotlin/LinuxX64.kt index 77dde3e175d..c4287bd56fa 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxX64Main/kotlin/LinuxX64.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/includedLib/src/linuxX64Main/kotlin/LinuxX64.kt @@ -1,5 +1,6 @@ package includedLib +@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun linuxX64( a: cinterop.a.StructA ) { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxArm64Main/kotlin/LinuxArm64.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxArm64Main/kotlin/LinuxArm64.kt index 19fccb66a0b..262445f3e1c 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxArm64Main/kotlin/LinuxArm64.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxArm64Main/kotlin/LinuxArm64.kt @@ -1,5 +1,6 @@ package lib +@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun linuxArm64( a: cinterop.lib.a.StructA ) { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxMain/kotlin/LinuxMain.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxMain/kotlin/LinuxMain.kt index 819fc9e10fc..decf27a80de 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxMain/kotlin/LinuxMain.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxMain/kotlin/LinuxMain.kt @@ -4,6 +4,7 @@ import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.alloc import kotlinx.cinterop.memScoped +@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun linuxMain() { cinterop.lib.a.a() @OptIn(ExperimentalForeignApi::class) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxX64Main/kotlin/LinuxX64.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxX64Main/kotlin/LinuxX64.kt index e7c2a144745..afb810f660d 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxX64Main/kotlin/LinuxX64.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/composite-build-with-cinterop-commonization/lib/src/linuxX64Main/kotlin/LinuxX64.kt @@ -1,5 +1,6 @@ package lib +@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun linuxX64( a: cinterop.lib.a.StructA ) { diff --git a/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/AnnotationsCommonizer.kt b/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/AnnotationsCommonizer.kt index 20ffad0383e..bd87b0f7b82 100644 --- a/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/AnnotationsCommonizer.kt +++ b/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/AnnotationsCommonizer.kt @@ -5,69 +5,48 @@ package org.jetbrains.kotlin.commonizer.core -import org.jetbrains.kotlin.commonizer.cir.* -import org.jetbrains.kotlin.commonizer.cir.CirConstantValue.* -import org.jetbrains.kotlin.commonizer.core.AnnotationsCommonizer.Companion.FALLBACK_MESSAGE -import org.jetbrains.kotlin.commonizer.utils.* -import kotlin.DeprecationLevel.WARNING +import org.jetbrains.kotlin.commonizer.cir.CirAnnotation +import org.jetbrains.kotlin.commonizer.cir.CirClassType +import org.jetbrains.kotlin.commonizer.cir.CirEntityId +import org.jetbrains.kotlin.commonizer.utils.COMMONIZER_OBJC_INTEROP_CALLABLE_ANNOTATION_ID +import org.jetbrains.kotlin.commonizer.utils.DEPRECATED_ANNOTATION_CLASS_ID +import org.jetbrains.kotlin.commonizer.utils.isObjCInteropCallableAnnotation -/** - * This is limited implementation of annotations commonizer. It helps to commonize only [kotlin.Deprecated] annotations. - */ -class AnnotationsCommonizer : AbstractStandardCommonizer, List>() { - private var deprecatedAnnotationCommonizer: DeprecatedAnnotationCommonizer? = null - private var deprecatedAnnotationCommonizerHasResult: Boolean = true +private typealias Annotations = Map - private val objCInteropCallableAnnotationCommonizer = ObjCInteropCallableAnnotationCommonizer.asCommonizer() - private var objCInteropCallableAnnotationCommonizerHasResult = true +object AnnotationsCommonizer : AssociativeCommonizer> { - override fun commonizationResult(): List { - val deprecatedAnnotation = if (deprecatedAnnotationCommonizerHasResult) - deprecatedAnnotationCommonizer?.result else null - - val objCInteropCallableAnnotations = if (objCInteropCallableAnnotationCommonizerHasResult) - objCInteropCallableAnnotationCommonizer.result else emptyList() - - return if (deprecatedAnnotation != null) { - objCInteropCallableAnnotations.plus(deprecatedAnnotation) - } else objCInteropCallableAnnotations - } - - override fun initialize(first: List) = Unit - - override fun doCommonizeWith(next: List): Boolean { - if (deprecatedAnnotationCommonizerHasResult) { - deprecatedAnnotationCommonizerHasResult = doCommonizeDeprecatedAnnotation(next) - } - - if (objCInteropCallableAnnotationCommonizerHasResult) { - objCInteropCallableAnnotationCommonizerHasResult = objCInteropCallableAnnotationCommonizer.commonizeWith(next) - } - - return true - } - - private fun doCommonizeDeprecatedAnnotation(next: List): Boolean { - val nextDeprecatedAnnotation = next.firstOrNull { it.type.classifierId == DEPRECATED_ANNOTATION_CLASS_ID } ?: return true - - val deprecatedAnnotationCommonizer = deprecatedAnnotationCommonizer - ?: DeprecatedAnnotationCommonizer().also { this.deprecatedAnnotationCommonizer = it } - - return deprecatedAnnotationCommonizer.commonizeWith(nextDeprecatedAnnotation) - } - - companion object { - internal const val FALLBACK_MESSAGE = "See concrete deprecation messages in actual declarations" - } -} - -object ObjCInteropCallableAnnotationCommonizer : AssociativeCommonizer> { override fun commonize(first: List, second: List): List { + val firstAnnotations = first.associateBy { it.type.classifierId } + val secondAnnotations = second.associateBy { it.type.classifierId } + + return setOfNotNull( + commonizedObjcCallableAnnotation(first, second), + commonizedDeprecatedAnnotation(firstAnnotations, secondAnnotations) + ).plus(commonizedSimpleAnnotations(firstAnnotations, secondAnnotations)).toList() + } + + private fun commonizedObjcCallableAnnotation(first: List, second: List): CirAnnotation? { return if (first.any { it.type.classifierId.isObjCInteropCallableAnnotation } && second.any { it.type.classifierId.isObjCInteropCallableAnnotation } - ) { - objCCallableAnnotationList - } else emptyList() + ) objCCallableAnnotation else null + } + + private fun commonizedSimpleAnnotations(first: Annotations, second: Annotations): List { + fun CirAnnotation.isSimple() = type.arguments.isEmpty() && !type.classifierId.isObjCInteropCallableAnnotation && + annotationValueArguments.isEmpty() && constantValueArguments.isEmpty() + + return first.values.filter { annotation -> + annotation.isSimple() && second[annotation.type.classifierId].let { secondAnnotation -> + secondAnnotation != null && secondAnnotation.isSimple() + } + } + } + + private fun commonizedDeprecatedAnnotation(first: Annotations, second: Annotations): CirAnnotation? { + val firstDeprecation = first[DEPRECATED_ANNOTATION_CLASS_ID] ?: return null + val secondDeprecation = second[DEPRECATED_ANNOTATION_CLASS_ID] ?: return null + return DeprecationAnnotationCommonizer.commonize(firstDeprecation, secondDeprecation) } private val objCCallableAnnotation = CirAnnotation.createInterned( @@ -78,186 +57,4 @@ object ObjCInteropCallableAnnotationCommonizer : AssociativeCommonizer { - private var level: DeprecationLevel? = null // null level means that state is empty - private var message: String? = null // null -> message is not equal - private lateinit var replaceWithExpression: String - private lateinit var replaceWithImports: List - - override val result: CirAnnotation - get() { - val level: DeprecationLevel = level ?: failInEmptyState() - val messageValue: StringValue = message.toDeprecationMessageValue() - - val constantValueArguments: Map = - if (level == WARNING) { - // don't populate with the default level value - compactMapOf(PROPERTY_NAME_MESSAGE, messageValue) - } else - compactMapOf( - PROPERTY_NAME_MESSAGE, messageValue, - PROPERTY_NAME_LEVEL, level.toDeprecationLevelValue() - ) - - val annotationValueArguments: Map = - if (replaceWithExpression.isEmpty() && replaceWithImports.isEmpty()) { - // don't populate with empty (default) ReplaceWith - emptyMap() - } else - compactMapOf(PROPERTY_NAME_REPLACE_WITH, replaceWithExpression.toReplaceWithValue(replaceWithImports)) - - return CirAnnotation.createInterned( - type = DEPRECATED_ANNOTATION_TYPE, - constantValueArguments = constantValueArguments, - annotationValueArguments = annotationValueArguments - ) - } - - override fun commonizeWith(next: CirAnnotation): Boolean { - val nextLevel: DeprecationLevel = next.getDeprecationLevel() ?: WARNING - val nextMessage: String = next.getDeprecationMessage().orEmpty() - val nextReplaceWith: CirAnnotation? = next.getReplaceWith() - val nextReplaceWithExpression: String = nextReplaceWith?.getReplaceWithExpression().orEmpty() - val nextReplaceWithImports: List = nextReplaceWith?.getReplaceWithImports().orEmpty() - - return if (level != null) { - doCommonizeWith(nextLevel, nextMessage, nextReplaceWithExpression, nextReplaceWithImports) - } else { - // empty, just fill in - initialize(nextLevel, nextMessage, nextReplaceWithExpression, nextReplaceWithImports) - true - } - } - - private fun initialize( - nextLevel: DeprecationLevel, - nextMessage: String?, - nextReplaceWithExpression: String, - nextReplaceWithImports: List - ) { - level = nextLevel - message = nextMessage - replaceWithExpression = nextReplaceWithExpression - replaceWithImports = nextReplaceWithImports - } - - private fun doCommonizeWith( - nextLevel: DeprecationLevel, - nextMessage: String?, - nextReplaceWithExpression: String, - nextReplaceWithImports: List - ): Boolean { - if (nextLevel.ordinal > level!!.ordinal) - level = nextLevel - - if (nextMessage != message) - message = null - - if (nextReplaceWithExpression != replaceWithExpression || nextReplaceWithImports != replaceWithImports) { - replaceWithExpression = "" - replaceWithImports = emptyList() - } - - return true - } - - @Suppress("NOTHING_TO_INLINE") - companion object { - private val PROPERTY_NAME_MESSAGE = CirName.create(Deprecated::message.name) - private val PROPERTY_NAME_REPLACE_WITH = CirName.create(Deprecated::replaceWith.name) - private val PROPERTY_NAME_LEVEL = CirName.create(Deprecated::level.name) - - private val PROPERTY_NAME_EXPRESSION = CirName.create(ReplaceWith::expression.name) - private val PROPERTY_NAME_IMPORTS = CirName.create(ReplaceWith::imports.name) - - // Optimization: Keep most frequently used message constants. - private val FREQUENTLY_USED_MESSAGE_VALUES: Map = listOf( - "Use constructor instead", - "Use factory method instead" - ).associateWith { StringValue(it) } - private val FALLBACK_MESSAGE_VALUE = StringValue(FALLBACK_MESSAGE) - - private val DEPRECATED_ANNOTATION_TYPE = buildAnnotationType(DEPRECATED_ANNOTATION_CLASS_ID) - private val REPLACE_WITH_ANNOTATION_TYPE = buildAnnotationType(CirEntityId.create("kotlin/ReplaceWith")) - - private val DEPRECATION_LEVEL_CLASS_ID = CirEntityId.create("kotlin/DeprecationLevel") - - // Optimization: Keep DeprecationLevel enum constants. - private val DEPRECATION_LEVEL_ENUM_ENTRY_VALUES: Map = DeprecationLevel.values().associate { - it.name to EnumValue(DEPRECATION_LEVEL_CLASS_ID, CirName.create(it.name)) - } - - private fun buildAnnotationType(classId: CirEntityId) = CirClassType.createInterned( - classId = classId, - outerType = null, - arguments = emptyList(), - isMarkedNullable = false - ) - - private fun CirAnnotation.getDeprecationMessage(): String? = constantValueArguments.getString(PROPERTY_NAME_MESSAGE) - - private fun String?.toDeprecationMessageValue(): StringValue = - if (this == null) - FALLBACK_MESSAGE_VALUE - else - FREQUENTLY_USED_MESSAGE_VALUES[this] ?: StringValue(this) - - private fun CirAnnotation.getDeprecationLevel(): DeprecationLevel? { - val enumEntryName = constantValueArguments.getEnumEntryName(PROPERTY_NAME_LEVEL) ?: return null - return DeprecationLevel.values().firstOrNull { it.name == enumEntryName } - } - - private fun DeprecationLevel.toDeprecationLevelValue(): EnumValue = - DEPRECATION_LEVEL_ENUM_ENTRY_VALUES.getValue(name) - - private fun CirAnnotation.getReplaceWith(): CirAnnotation? = - annotationValueArguments.getAnnotation(PROPERTY_NAME_REPLACE_WITH) - - private fun CirAnnotation.getReplaceWithExpression(): String? = - constantValueArguments.getString(PROPERTY_NAME_EXPRESSION) - - private fun CirAnnotation.getReplaceWithImports(): List? = - constantValueArguments.getStringArray(PROPERTY_NAME_IMPORTS) - - private fun String.toReplaceWithValue(imports: List): CirAnnotation = - createReplaceWithAnnotation(this, imports) - - private inline fun Map.getString(name: CirName): String? = - (this[name] as? StringValue)?.value - - private inline fun Map.getEnumEntryName(name: CirName): String? = - (this[name] as? EnumValue)?.enumEntryName?.name - - private inline fun Map.getAnnotation(name: CirName): CirAnnotation? = - this[name] - - private inline fun Map.getStringArray(name: CirName): List? { - val elements: List = (this[name] as? ArrayValue)?.elements ?: return null - if (elements.isEmpty()) return emptyList() - - val result = ArrayList(elements.size) - for (element in elements) { - if (element is StringValue) { - result += element.value - } else - return null - } - - return result - } - - private inline fun createReplaceWithAnnotation(expression: String, imports: List): CirAnnotation = - CirAnnotation.createInterned( - type = REPLACE_WITH_ANNOTATION_TYPE, - constantValueArguments = compactMapOf( - PROPERTY_NAME_EXPRESSION, StringValue(expression), - PROPERTY_NAME_IMPORTS, ArrayValue(imports.compactMap(::StringValue)) - ), - annotationValueArguments = emptyMap() - ) - } } diff --git a/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/ClassConstructorCommonizer.kt b/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/ClassConstructorCommonizer.kt index 5c98e6cd168..a51f3d94518 100644 --- a/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/ClassConstructorCommonizer.kt +++ b/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/ClassConstructorCommonizer.kt @@ -17,7 +17,7 @@ class ClassConstructorCommonizer( private val visibility = VisibilityCommonizer.equalizing() private val typeParameterListCommonizer = TypeParameterListCommonizer(typeCommonizer) private val valueParametersCommonizer = CallableValueParametersCommonizer(typeCommonizer) - private val annotationsCommonizer: AnnotationsCommonizer = AnnotationsCommonizer() + private val annotationsCommonizer = AnnotationsCommonizer.asCommonizer() override fun commonizationResult(): CirClassConstructor? { val valueParameters = valueParametersCommonizer.result ?: return null diff --git a/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/DeprecationAnnotationCommonizer.kt b/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/DeprecationAnnotationCommonizer.kt new file mode 100644 index 00000000000..9ba10834225 --- /dev/null +++ b/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/DeprecationAnnotationCommonizer.kt @@ -0,0 +1,162 @@ +/* + * 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.commonizer.core + +import org.jetbrains.kotlin.commonizer.cir.* +import org.jetbrains.kotlin.commonizer.utils.DEPRECATED_ANNOTATION_CLASS_ID +import org.jetbrains.kotlin.commonizer.utils.compactMap +import org.jetbrains.kotlin.commonizer.utils.compactMapOf + +@Suppress("NOTHING_TO_INLINE") +object DeprecationAnnotationCommonizer : AssociativeCommonizer { + override fun commonize(first: CirAnnotation, second: CirAnnotation): CirAnnotation { + val deprecationLevel = run { + val firstLevel = first.getDeprecationLevel() ?: DeprecationLevel.WARNING + val secondLevel = second.getDeprecationLevel() ?: DeprecationLevel.WARNING + if (secondLevel.ordinal > firstLevel.ordinal) secondLevel else firstLevel + } + + val deprecationMessage = run { + val firstMessage = first.getDeprecationMessage() ?: return@run null + val secondMessage = second.getDeprecationMessage() ?: return@run null + if (firstMessage == secondMessage) firstMessage else null + } + + val replaceWith = run { + val firstReplaceWith = first.getReplaceWith() ?: return@run null + val secondReplaceWith = second.getReplaceWith() ?: return@run null + + val firstReplaceWithExpression = firstReplaceWith.getReplaceWithExpression().orEmpty() + val secondReplaceWithExpression = secondReplaceWith.getReplaceWithExpression().orEmpty() + + val firstReplaceWithImports = firstReplaceWith.getReplaceWithImports().orEmpty() + val secondReplaceWithImports = secondReplaceWith.getReplaceWithImports().orEmpty() + + if ( + firstReplaceWithExpression == secondReplaceWithExpression && + firstReplaceWithImports == secondReplaceWithImports && + /* Empty replace with */ + (firstReplaceWithExpression.isNotEmpty() || firstReplaceWithImports.isNotEmpty()) + ) firstReplaceWithExpression.toReplaceWithValue(firstReplaceWithImports) else null + } + + + val constantValueArguments: Map = if (deprecationLevel == DeprecationLevel.WARNING) { + // don't populate with the default level value + compactMapOf(PROPERTY_NAME_MESSAGE, deprecationMessage.toDeprecationMessageValue()) + } else compactMapOf( + PROPERTY_NAME_MESSAGE, deprecationMessage.toDeprecationMessageValue(), + PROPERTY_NAME_LEVEL, deprecationLevel.toDeprecationLevelValue() + ) + + val annotationValueArguments: Map = if (replaceWith == null) { + // don't populate with empty (default) ReplaceWith + emptyMap() + } else compactMapOf(PROPERTY_NAME_REPLACE_WITH, replaceWith) + + return CirAnnotation.createInterned( + type = DEPRECATED_ANNOTATION_TYPE, + constantValueArguments = constantValueArguments, + annotationValueArguments = annotationValueArguments + ) + } + + private val PROPERTY_NAME_MESSAGE = CirName.create(Deprecated::message.name) + private val PROPERTY_NAME_REPLACE_WITH = CirName.create(Deprecated::replaceWith.name) + private val PROPERTY_NAME_LEVEL = CirName.create(Deprecated::level.name) + + private val PROPERTY_NAME_EXPRESSION = CirName.create(ReplaceWith::expression.name) + private val PROPERTY_NAME_IMPORTS = CirName.create(ReplaceWith::imports.name) + + internal const val FALLBACK_MESSAGE = "See concrete deprecation messages in actual declarations" + + + // Optimization: Keep most frequently used message constants. + private val FREQUENTLY_USED_MESSAGE_VALUES: Map = listOf( + "Use constructor instead", + "Use factory method instead" + ).associateWith { CirConstantValue.StringValue(it) } + private val FALLBACK_MESSAGE_VALUE = CirConstantValue.StringValue(FALLBACK_MESSAGE) + + private val DEPRECATED_ANNOTATION_TYPE = buildAnnotationType(DEPRECATED_ANNOTATION_CLASS_ID) + private val REPLACE_WITH_ANNOTATION_TYPE = buildAnnotationType(CirEntityId.create("kotlin/ReplaceWith")) + + private val DEPRECATION_LEVEL_CLASS_ID = CirEntityId.create("kotlin/DeprecationLevel") + + // Optimization: Keep DeprecationLevel enum constants. + private val DEPRECATION_LEVEL_ENUM_ENTRY_VALUES: Map = DeprecationLevel.entries.associate { + it.name to CirConstantValue.EnumValue(DEPRECATION_LEVEL_CLASS_ID, CirName.create(it.name)) + } + + private fun buildAnnotationType(classId: CirEntityId) = CirClassType.createInterned( + classId = classId, + outerType = null, + arguments = emptyList(), + isMarkedNullable = false + ) + + private fun CirAnnotation.getDeprecationMessage(): String? = constantValueArguments.getString(PROPERTY_NAME_MESSAGE) + + private fun String?.toDeprecationMessageValue(): CirConstantValue.StringValue = + if (this == null) + FALLBACK_MESSAGE_VALUE + else + FREQUENTLY_USED_MESSAGE_VALUES[this] ?: CirConstantValue.StringValue(this) + + private fun CirAnnotation.getDeprecationLevel(): DeprecationLevel? { + val enumEntryName = constantValueArguments.getEnumEntryName(PROPERTY_NAME_LEVEL) ?: return null + return DeprecationLevel.entries.firstOrNull { it.name == enumEntryName } + } + + private fun DeprecationLevel.toDeprecationLevelValue(): CirConstantValue.EnumValue = + DEPRECATION_LEVEL_ENUM_ENTRY_VALUES.getValue(name) + + private fun CirAnnotation.getReplaceWith(): CirAnnotation? = + annotationValueArguments.getAnnotation(PROPERTY_NAME_REPLACE_WITH) + + private fun CirAnnotation.getReplaceWithExpression(): String? = + constantValueArguments.getString(PROPERTY_NAME_EXPRESSION) + + private fun CirAnnotation.getReplaceWithImports(): List? = + constantValueArguments.getStringArray(PROPERTY_NAME_IMPORTS) + + private fun String.toReplaceWithValue(imports: List): CirAnnotation = + createReplaceWithAnnotation(this, imports) + + private inline fun Map.getString(name: CirName): String? = + (this[name] as? CirConstantValue.StringValue)?.value + + private inline fun Map.getEnumEntryName(name: CirName): String? = + (this[name] as? CirConstantValue.EnumValue)?.enumEntryName?.name + + private inline fun Map.getAnnotation(name: CirName): CirAnnotation? = + this[name] + + private inline fun Map.getStringArray(name: CirName): List? { + val elements: List = (this[name] as? CirConstantValue.ArrayValue)?.elements ?: return null + if (elements.isEmpty()) return emptyList() + + val result = ArrayList(elements.size) + for (element in elements) { + if (element is CirConstantValue.StringValue) { + result += element.value + } else + return null + } + + return result + } + + private inline fun createReplaceWithAnnotation(expression: String, imports: List): CirAnnotation = + CirAnnotation.createInterned( + type = REPLACE_WITH_ANNOTATION_TYPE, + constantValueArguments = compactMapOf( + PROPERTY_NAME_EXPRESSION, CirConstantValue.StringValue(expression), + PROPERTY_NAME_IMPORTS, CirConstantValue.ArrayValue(imports.compactMap(CirConstantValue::StringValue)) + ), + annotationValueArguments = emptyMap() + ) +} diff --git a/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/FunctionCommonizer.kt b/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/FunctionCommonizer.kt index 2cf3d0a18d3..2db4b62f074 100644 --- a/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/FunctionCommonizer.kt +++ b/native/commonizer/src/org/jetbrains/kotlin/commonizer/core/FunctionCommonizer.kt @@ -16,7 +16,7 @@ class FunctionCommonizer( val functionOrProperty = functionOrPropertyBaseCommonizer(values) ?: return null val valueParametersResult = CallableValueParametersCommonizer(typeCommonizer).commonize(values) ?: return null return CirFunction( - annotations = AnnotationsCommonizer().commonize(values.map { it.annotations }) + annotations = AnnotationsCommonizer.commonize(values.map { it.annotations.toList() }) ?.plus(functionOrProperty.additionalAnnotations) ?: return null, name = values.first().name, diff --git a/native/commonizer/tests/org/jetbrains/kotlin/commonizer/core/AnnotationsCommonizerTest.kt b/native/commonizer/tests/org/jetbrains/kotlin/commonizer/core/AnnotationsCommonizerTest.kt index c990ee7ce0d..cc4b5200881 100644 --- a/native/commonizer/tests/org/jetbrains/kotlin/commonizer/core/AnnotationsCommonizerTest.kt +++ b/native/commonizer/tests/org/jetbrains/kotlin/commonizer/core/AnnotationsCommonizerTest.kt @@ -42,7 +42,7 @@ class AnnotationsCommonizerTest : AbstractCommonizerTest, Li @Test fun noRelevantAnnotations3() = doTestSuccess( - expected = emptyList(), + expected = listOf(mockAnnotation("org/sample/Foo")), listOf(mockAnnotation("org/sample/Foo")), listOf(mockAnnotation("org/sample/Foo")), listOf(mockAnnotation("org/sample/Foo")) @@ -58,7 +58,7 @@ class AnnotationsCommonizerTest : AbstractCommonizerTest, Li @Test fun noRelevantAnnotations5() = doTestSuccess( - expected = emptyList(), + expected = listOf(mockAnnotation("kotlin/PublishedApi")), listOf(mockAnnotation("kotlin/PublishedApi")), listOf(mockAnnotation("kotlin/PublishedApi")), listOf(mockAnnotation("kotlin/PublishedApi")) @@ -74,7 +74,7 @@ class AnnotationsCommonizerTest : AbstractCommonizerTest, Li @Test fun differentMessages() = doTestSuccess( - expected = listOf(mockDeprecated(message = AnnotationsCommonizer.FALLBACK_MESSAGE)), + expected = listOf(mockDeprecated(message = DeprecationAnnotationCommonizer.FALLBACK_MESSAGE)), listOf(mockDeprecated(message = "please don't use it because ...")), listOf(mockDeprecated(message = "it should not be used due to ...")), listOf(mockDeprecated(message = "please don't use it because ...")) @@ -276,7 +276,7 @@ class AnnotationsCommonizerTest : AbstractCommonizerTest, Li ) ) - override fun createCommonizer() = AnnotationsCommonizer() + override fun createCommonizer() = AnnotationsCommonizer.asCommonizer() } private fun mockAnnotation( diff --git a/native/commonizer/tests/org/jetbrains/kotlin/commonizer/hierarchical/HierarchicalFunctionCommonizationTest.kt b/native/commonizer/tests/org/jetbrains/kotlin/commonizer/hierarchical/HierarchicalFunctionCommonizationTest.kt index 98f8ced31c4..a2b21f075a0 100644 --- a/native/commonizer/tests/org/jetbrains/kotlin/commonizer/hierarchical/HierarchicalFunctionCommonizationTest.kt +++ b/native/commonizer/tests/org/jetbrains/kotlin/commonizer/hierarchical/HierarchicalFunctionCommonizationTest.kt @@ -135,4 +135,37 @@ class HierarchicalFunctionCommonizationTest : AbstractInlineSourcesCommonization result.assertCommonized("(a, b)", "expect fun x(): ABCD") result.assertCommonized("((a,b), c)", "expect fun x(): ABCD") } + + fun `test function with simple annotation`() { + val result = commonize { + outputTarget("(a, b)") + registerDependency("a", "b", "(a, b)") { source("annotation class FooAnnotation") } + simpleSingleSourceTarget("a", "@FooAnnotation fun x() = Unit") + simpleSingleSourceTarget("b", "@FooAnnotation fun x() = Unit") + } + + result.assertCommonized("(a, b)", "@FooAnnotation expect fun x()") + } + + fun `test function with non-simple annotation - 1`() { + val result = commonize { + outputTarget("(a, b)") + registerDependency("a", "b", "(a, b)") { source("annotation class FooAnnotation(val param: String)") } + simpleSingleSourceTarget("a", """@FooAnnotation("a") fun x() = Unit""") + simpleSingleSourceTarget("b", """@FooAnnotation("b") fun x() = Unit""") + } + + result.assertCommonized("(a, b)", "expect fun x()") + } + + fun `test function with non-simple annotation - 2`() { + val result = commonize { + outputTarget("(a, b)") + registerDependency("a", "b", "(a, b)") { source("annotation class FooAnnotation(val param: String)") } + simpleSingleSourceTarget("a", """@FooAnnotation("a") fun x() = Unit""") + simpleSingleSourceTarget("b", """@FooAnnotation("b") fun x() = Unit""") + } + + result.assertCommonized("(a, b)", "expect fun x()") + } }