diff --git a/compiler/serialization/src/org/jetbrains/kotlin/serialization/DescriptorSerializer.kt b/compiler/serialization/src/org/jetbrains/kotlin/serialization/DescriptorSerializer.kt index dd7f07648f3..f1e24767171 100644 --- a/compiler/serialization/src/org/jetbrains/kotlin/serialization/DescriptorSerializer.kt +++ b/compiler/serialization/src/org/jetbrains/kotlin/serialization/DescriptorSerializer.kt @@ -28,7 +28,10 @@ import org.jetbrains.kotlin.protobuf.MessageLite import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumEntry import org.jetbrains.kotlin.resolve.MemberComparator +import org.jetbrains.kotlin.resolve.constants.EnumValue +import org.jetbrains.kotlin.resolve.constants.IntValue import org.jetbrains.kotlin.resolve.constants.NullValue +import org.jetbrains.kotlin.resolve.constants.StringValue import org.jetbrains.kotlin.serialization.deserialization.descriptors.VersionRequirement import org.jetbrains.kotlin.types.* import org.jetbrains.kotlin.types.typeUtil.contains @@ -134,6 +137,11 @@ class DescriptorSerializer private constructor( builder.typeTable = typeTableProto } + val requirement = serializeVersionRequirement(classDescriptor) + if (requirement != null) { + builder.versionRequirement = requirement + } + val versionRequirementTableProto = versionRequirementTable.serialize() if (versionRequirementTableProto != null) { builder.versionRequirementTable = versionRequirementTableProto @@ -216,7 +224,11 @@ class DescriptorSerializer private constructor( } } - if (descriptor.isSuspendOrHasSuspendTypesInSignature()) { + val requirement = serializeVersionRequirement(descriptor) + if (requirement != null) { + builder.versionRequirement = requirement + } + else if (descriptor.isSuspendOrHasSuspendTypesInSignature()) { builder.versionRequirement = writeVersionRequirement(LanguageFeature.Coroutines) } @@ -273,7 +285,11 @@ class DescriptorSerializer private constructor( } } - if (descriptor.isSuspendOrHasSuspendTypesInSignature()) { + val requirement = serializeVersionRequirement(descriptor) + if (requirement != null) { + builder.versionRequirement = requirement + } + else if (descriptor.isSuspendOrHasSuspendTypesInSignature()) { builder.versionRequirement = writeVersionRequirement(LanguageFeature.Coroutines) } @@ -296,7 +312,11 @@ class DescriptorSerializer private constructor( builder.addValueParameter(local.valueParameter(valueParameterDescriptor)) } - if (descriptor.isSuspendOrHasSuspendTypesInSignature()) { + val requirement = serializeVersionRequirement(descriptor) + if (requirement != null) { + builder.versionRequirement = requirement + } + else if (descriptor.isSuspendOrHasSuspendTypesInSignature()) { builder.versionRequirement = writeVersionRequirement(LanguageFeature.Coroutines) } @@ -346,6 +366,11 @@ class DescriptorSerializer private constructor( builder.setExpandedType(local.type(expandedType)) } + val requirement = serializeVersionRequirement(descriptor) + if (requirement != null) { + builder.versionRequirement = requirement + } + builder.addAllAnnotation(descriptor.annotations.map { extension.annotationSerializer.serializeAnnotation(it) }) return builder @@ -578,6 +603,44 @@ class DescriptorSerializer private constructor( return versionRequirementTable[requirement] } + // Returns index into versionRequirementTable, or null if there's no @RequireKotlin on the descriptor + private fun serializeVersionRequirement(descriptor: DeclarationDescriptor): Int? { + val annotation = descriptor.annotations.findAnnotation(RequireKotlinNames.FQ_NAME) ?: return null + val args = annotation.allValueArguments + + val versionString = (args[RequireKotlinNames.VERSION] as? StringValue)?.value ?: return null + val matchResult = RequireKotlinNames.VERSION_REGEX.matchEntire(versionString) ?: return null + + val major = matchResult.groupValues.getOrNull(1)?.toIntOrNull() ?: return null + val minor = matchResult.groupValues.getOrNull(2)?.toIntOrNull() ?: 0 + val patch = matchResult.groupValues.getOrNull(3)?.toIntOrNull() ?: 0 + + val proto = ProtoBuf.VersionRequirement.newBuilder() + VersionRequirement.Version(major, minor, patch).encode( + writeVersion = { proto.version = it }, + writeVersionFull = { proto.versionFull = it } + ) + + val message = (args[RequireKotlinNames.MESSAGE] as? StringValue)?.value + if (message != null) { + proto.message = stringTable.getStringIndex(message) + } + + val level = (args[RequireKotlinNames.LEVEL] as? EnumValue)?.value?.name?.asString() + when (level) { + DeprecationLevel.ERROR.toString() -> { /* ERROR is the default level */ } + DeprecationLevel.WARNING.toString() -> proto.level = ProtoBuf.VersionRequirement.Level.WARNING + DeprecationLevel.HIDDEN.toString() -> proto.level = ProtoBuf.VersionRequirement.Level.HIDDEN + } + + val errorCode = (args[RequireKotlinNames.ERROR_CODE] as? IntValue)?.value + if (errorCode != null && errorCode != -1) { + proto.errorCode = errorCode + } + + return versionRequirementTable[proto] + } + private fun getClassifierId(descriptor: ClassifierDescriptorWithTypeParameters): Int = stringTable.getFqNameIndex(descriptor) @@ -587,6 +650,17 @@ class DescriptorSerializer private constructor( private fun getTypeParameterId(descriptor: TypeParameterDescriptor): Int = typeParameters.intern(descriptor) + private object RequireKotlinNames { + val FQ_NAME = FqName("kotlin.internal.RequireKotlin") + + val VERSION = Name.identifier("version") + val MESSAGE = Name.identifier("message") + val LEVEL = Name.identifier("level") + val ERROR_CODE = Name.identifier("errorCode") + + val VERSION_REGEX: Regex = "(0|[1-9][0-9]*)".let { number -> Regex("$number\\.$number(\\.$number)?") } + } + companion object { @JvmStatic fun createTopLevel(extension: SerializerExtension): DescriptorSerializer { diff --git a/compiler/testData/versionRequirement/languageVersionViaAnnotation.kt b/compiler/testData/versionRequirement/languageVersionViaAnnotation.kt new file mode 100644 index 00000000000..b2e0f477ffc --- /dev/null +++ b/compiler/testData/versionRequirement/languageVersionViaAnnotation.kt @@ -0,0 +1,18 @@ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +package test + +import kotlin.internal.RequireKotlin + +@RequireKotlin("1.1", "message", DeprecationLevel.WARNING, 42) +class Klass + +class Konstructor @RequireKotlin("1.1", "message", DeprecationLevel.WARNING, 42) constructor() + +@RequireKotlin("1.1", "message", DeprecationLevel.WARNING, 42) +typealias Typealias = String + +@RequireKotlin("1.1", "message", DeprecationLevel.WARNING, 42) +fun function() {} + +@RequireKotlin("1.1", "message", DeprecationLevel.WARNING, 42) +val property = "" diff --git a/compiler/tests/org/jetbrains/kotlin/serialization/VersionRequirementTest.kt b/compiler/tests/org/jetbrains/kotlin/serialization/VersionRequirementTest.kt index b8c6ae92f78..fab1d35e26a 100644 --- a/compiler/tests/org/jetbrains/kotlin/serialization/VersionRequirementTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/serialization/VersionRequirementTest.kt @@ -26,6 +26,7 @@ import org.jetbrains.kotlin.jvm.compiler.LoadDescriptorUtil import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.lazy.JvmResolveUtil import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered +import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedMemberDescriptor import org.jetbrains.kotlin.serialization.deserialization.descriptors.VersionRequirement import org.jetbrains.kotlin.test.ConfigurationKind @@ -36,7 +37,6 @@ import java.io.File class VersionRequirementTest : TestCaseWithTmpdir() { fun doTest( - fileName: String, expectedVersionRequirement: VersionRequirement.Version, expectedLevel: DeprecationLevel, expectedMessage: String?, @@ -44,7 +44,7 @@ class VersionRequirementTest : TestCaseWithTmpdir() { vararg fqNames: String ) { LoadDescriptorUtil.compileKotlinToDirAndGetModule( - listOf(File(fileName)), tmpdir, + listOf(File("compiler/testData/versionRequirement/${getTestName(true)}.kt")), tmpdir, KotlinTestUtils.createEnvironmentWithMockJdkAndIdeaAnnotations(testRootDisposable) ) @@ -57,11 +57,11 @@ class VersionRequirementTest : TestCaseWithTmpdir() { ) fun check(descriptor: DeclarationDescriptor) { - if (descriptor !is DeserializedMemberDescriptor) { - throw AssertionError("Not a deserialized descriptor: $descriptor") - } - - val requirement = descriptor.versionRequirement ?: throw AssertionError("No VersionRequirement for $descriptor") + val requirement = when (descriptor) { + is DeserializedMemberDescriptor -> descriptor.versionRequirement + is DeserializedClassDescriptor -> descriptor.versionRequirement + else -> throw AssertionError("Unknown descriptor: $descriptor") + } ?: throw AssertionError("No VersionRequirement for $descriptor") assertEquals(expectedVersionRequirement, requirement.version) assertEquals(expectedLevel, requirement.level) @@ -96,8 +96,7 @@ class VersionRequirementTest : TestCaseWithTmpdir() { } fun testSuspendFun() { - doTest("compiler/testData/versionRequirement/suspendFun.kt", - VersionRequirement.Version(1, 1), DeprecationLevel.ERROR, null, null, + doTest(VersionRequirement.Version(1, 1), DeprecationLevel.ERROR, null, null, "test.topLevel", "test.Foo.member", "test.Foo.", @@ -108,4 +107,14 @@ class VersionRequirementTest : TestCaseWithTmpdir() { "test.asyncVal" ) } + + fun testLanguageVersionViaAnnotation() { + doTest(VersionRequirement.Version(1, 1), DeprecationLevel.WARNING, "message", 42, + "test.Klass", + "test.Konstructor.", + "test.Typealias", + "test.function", + "test.property" + ) + } } diff --git a/libraries/stdlib/src/kotlin/internal/Annotations.kt b/libraries/stdlib/src/kotlin/internal/Annotations.kt index a6b986bfb75..00db16468d7 100644 --- a/libraries/stdlib/src/kotlin/internal/Annotations.kt +++ b/libraries/stdlib/src/kotlin/internal/Annotations.kt @@ -65,3 +65,22 @@ internal annotation class InlineOnly @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.BINARY) internal annotation class DynamicExtension + +/** + * Specifies that this declaration is only completely supported since the specified version. + * + * The Kotlin compiler of an earlier version is going to report a diagnostic on usages of this declaration. + * The diagnostic message can be specified with [message], or via [errorCode] (takes less space, but might not be immediately clear + * to the user). The diagnostic severity can be specified with [level]: WARNING/ERROR mean that either a warning or an error + * is going to be reported, HIDDEN means that the declaration is going to be removed from resolution completely. + * + * This annotation is erased at compile time; its arguments are stored in a more compact form in the Kotlin metadata. + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.TYPEALIAS) +@Retention(AnnotationRetention.SOURCE) +internal annotation class RequireKotlin( + val version: String, + val message: String = "", + val level: DeprecationLevel = DeprecationLevel.ERROR, + val errorCode: Int = -1 +)