From df5f38fec878018b012408d0e510482ff5e57d8c Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Thu, 16 Jul 2020 22:10:14 +0300 Subject: [PATCH] Implement reading kx.serialization runtime metadata from jar manifest That allows to check whether current compiler plugin can work with a given runtime version. Implementation-Version can help to detect whether runtime version is too low, and Require-Kotlin-Version detects whether runtime version is too high. Corresponding diagnostic for Require-Kotlin included. Implement minimal runtime version check for kotlinx.serialization plugin. #KT-40036 Fixed Cache result from getVersionsForCurrentModule in BindingTrace. Hide check in the IDE, because caching in trace does not work there. --- .../diagnostic/SerializationErrors.java | 8 +-- .../SerializationPluginDeclarationChecker.kt | 51 ++++++++++++++- .../SerializationPluginErrorsRendering.kt | 17 +++++ .../compiler/diagnostic/VersionReader.kt | 62 +++++++++++++++++++ .../kotlinx/serialization/RuntimeSearch.kt | 12 +++- ...erializationPluginIDEDeclarationChecker.kt | 5 +- 6 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/VersionReader.kt diff --git a/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationErrors.java b/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationErrors.java index 5616ece19ec..63730db6d83 100644 --- a/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationErrors.java +++ b/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationErrors.java @@ -6,10 +6,7 @@ package org.jetbrains.kotlinx.serialization.compiler.diagnostic; import com.intellij.psi.PsiElement; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory0; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory2; -import org.jetbrains.kotlin.diagnostics.Errors; +import org.jetbrains.kotlin.diagnostics.*; import org.jetbrains.kotlin.psi.KtAnnotationEntry; import org.jetbrains.kotlin.types.KotlinType; @@ -34,6 +31,9 @@ public interface SerializationErrors { DiagnosticFactory0 INCORRECT_TRANSIENT = DiagnosticFactory0.create(WARNING); + DiagnosticFactory3 REQUIRED_KOTLIN_TOO_HIGH = DiagnosticFactory3.create(ERROR); + DiagnosticFactory3 PROVIDED_RUNTIME_TOO_LOW = DiagnosticFactory3.create(ERROR); + @SuppressWarnings("UnusedDeclaration") Object _initializer = new Object() { { diff --git a/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginDeclarationChecker.kt b/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginDeclarationChecker.kt index 2729b12a3fb..08b201f3ed8 100644 --- a/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginDeclarationChecker.kt +++ b/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginDeclarationChecker.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlinx.serialization.compiler.diagnostic import com.intellij.psi.PsiElement import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.config.KotlinCompilerVersion import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.diagnostics.DiagnosticFactory0 import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi @@ -17,6 +18,7 @@ import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces +import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.resolve.hasBackingField import org.jetbrains.kotlin.resolve.isInlineClassType import org.jetbrains.kotlin.resolve.jvm.annotations.TRANSIENT_ANNOTATION_FQ_NAME @@ -40,12 +42,55 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker { if (!canBeSerializedInternally(descriptor, declaration, context.trace)) return if (declaration !is KtPureClassOrObject) return + if (!isIde) { + // In IDE, BindingTrace is recreated each time code is modified, effectively resulting in JAR manifest read every time user types + // something, which may be very slow. So we perform this check only during CLI/Gradle compilation. + VersionReader.getVersionsForCurrentModuleFromTrace(descriptor.module, context.trace)?.let { + checkMinKotlin(it, descriptor, context.trace) + checkMinRuntime(it, descriptor, context.trace) + } + } val props = buildSerializableProperties(descriptor, context.trace) ?: return checkCorrectTransientAnnotationIsUsed(descriptor, props.serializableProperties, context.trace) checkTransients(declaration, context.trace) analyzePropertiesSerializers(context.trace, descriptor, props.serializableProperties) } + private fun checkMinRuntime(versions: VersionReader.RuntimeVersions, descriptor: ClassDescriptor, trace: BindingTrace) { + // if RuntimeVersions are present, but implementation version is not, + // it means that we are reading from jar which does not have this manifest parameter - a pre-1.0 serialization runtime. + // For non-JAR distributions (klib, js) this method is not invoked, since getVersionsForCurrentModule + // unable to read from them + if (!versions.implementationVersionMatchSupported()) { + descriptor.onSerializableAnnotation { + trace.report( + SerializationErrors.PROVIDED_RUNTIME_TOO_LOW.on( + it, + versions.implementationVersion?.toString() ?: "too low", + KotlinCompilerVersion.getVersion() ?: "unknown", + VersionReader.MINIMAL_SUPPORTED_VERSION.toString(), + ) + ) + } + } + } + + private fun checkMinKotlin(versions: VersionReader.RuntimeVersions, descriptor: ClassDescriptor, trace: BindingTrace) { + if (versions.currentCompilerMatchRequired()) return + descriptor.onSerializableAnnotation { + trace.report( + SerializationErrors.REQUIRED_KOTLIN_TOO_HIGH.on( + it, + KotlinCompilerVersion.getVersion() ?: "too low", + versions.implementationVersion?.toString() ?: "unknown", + versions.requireKotlinVersion?.toString() ?: "N/A", + ) + ) + } + } + + protected open val isIde: Boolean get() = false + private fun checkCorrectTransientAnnotationIsUsed( descriptor: ClassDescriptor, properties: List, @@ -127,7 +172,7 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker { val namesSet = mutableSetOf() props.serializableProperties.forEach { if (!namesSet.add(it.name)) { - descriptor.safeReport { a -> + descriptor.onSerializableAnnotation { a -> trace.report(SerializationErrors.DUPLICATE_SERIAL_NAME.on(a, it.name)) } } @@ -238,12 +283,12 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker { ) } - private inline fun ClassDescriptor.safeReport(report: (KtAnnotationEntry) -> Unit) { + private inline fun ClassDescriptor.onSerializableAnnotation(report: (KtAnnotationEntry) -> Unit) { findSerializableAnnotationDeclaration()?.let(report) } private fun BindingTrace.reportOnSerializableAnnotation(descriptor: ClassDescriptor, error: DiagnosticFactory0) { - descriptor.safeReport { e -> + descriptor.onSerializableAnnotation { e -> report(error.on(e)) } } diff --git a/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginErrorsRendering.kt b/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginErrorsRendering.kt index f071e10e63f..a636eebbe02 100644 --- a/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginErrorsRendering.kt +++ b/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginErrorsRendering.kt @@ -69,5 +69,22 @@ object SerializationPluginErrorsRendering : DefaultErrorMessages.Extension { SerializationErrors.INCORRECT_TRANSIENT, "@kotlin.jvm.Transient does not affect @Serializable classes. Please use @kotlinx.serialization.Transient instead." ) + MAP.put( + SerializationErrors.REQUIRED_KOTLIN_TOO_HIGH, + "Your current Kotlin version is {0}, while kotlinx.serialization core runtime {1} requires at least Kotlin {2}. " + + "Please update your Kotlin compiler and IDE plugin.", + Renderers.STRING, + Renderers.STRING, + Renderers.STRING + ) + + MAP.put( + SerializationErrors.PROVIDED_RUNTIME_TOO_LOW, + "Your current kotlinx.serialization core version is {0}, while current Kotlin compiler plugin {1} requires at least {2}. " + + "Please update your kotlinx.serialization runtime dependency.", + Renderers.STRING, + Renderers.STRING, + Renderers.STRING + ) } } \ No newline at end of file diff --git a/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/VersionReader.kt b/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/VersionReader.kt new file mode 100644 index 00000000000..749b0a74862 --- /dev/null +++ b/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/VersionReader.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 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.kotlinx.serialization.compiler.diagnostic + +import com.intellij.openapi.util.io.JarUtil +import org.jetbrains.kotlin.config.ApiVersion +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.load.kotlin.KotlinJvmBinarySourceElement +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.util.slicedMap.Slices +import org.jetbrains.kotlin.util.slicedMap.WritableSlice +import org.jetbrains.kotlinx.serialization.compiler.resolve.SerialEntityNames +import org.jetbrains.kotlinx.serialization.compiler.resolve.getClassFromSerializationPackage +import java.io.File +import java.util.jar.Attributes + +object VersionReader { + data class RuntimeVersions(val implementationVersion: ApiVersion?, val requireKotlinVersion: ApiVersion?) { + fun currentCompilerMatchRequired(): Boolean { + val current = requireNotNull(KotlinCompilerVersion.getVersion()?.let(ApiVersion.Companion::parse)) + return requireKotlinVersion == null || requireKotlinVersion <= current + } + + fun implementationVersionMatchSupported(): Boolean { + return implementationVersion != null && implementationVersion >= MINIMAL_SUPPORTED_VERSION + } + } + + fun getVersionsFromManifest(runtimeLibraryPath: File): RuntimeVersions { + val version = JarUtil.getJarAttribute(runtimeLibraryPath, Attributes.Name.IMPLEMENTATION_VERSION)?.let(ApiVersion.Companion::parse) + val kotlinVersion = JarUtil.getJarAttribute(runtimeLibraryPath, REQUIRE_KOTLIN_VERSION)?.let(ApiVersion.Companion::parse) + return RuntimeVersions(version, kotlinVersion) + } + + val MINIMAL_SUPPORTED_VERSION = ApiVersion.parse("1.0-M1-SNAPSHOT")!! + + private val REQUIRE_KOTLIN_VERSION = Attributes.Name("Require-Kotlin-Version") + private const val CLASS_SUFFIX = "!/kotlinx/serialization/KSerializer.class" + + private val VERSIONS_SLICE: WritableSlice = Slices.createSimpleSlice() + + fun getVersionsForCurrentModuleFromTrace(module: ModuleDescriptor, trace: BindingTrace): RuntimeVersions? { + trace.get(VERSIONS_SLICE, module)?.let { return it } + val versions = getVersionsForCurrentModule(module) ?: return null + trace.record(VERSIONS_SLICE, module, versions) + return versions + } + + fun getVersionsForCurrentModule(module: ModuleDescriptor): RuntimeVersions? { + val markerClass = module.getClassFromSerializationPackage(SerialEntityNames.KSERIALIZER_CLASS) + val location = (markerClass.source as? KotlinJvmBinarySourceElement)?.binaryClass?.location ?: return null + val jarFile = location.removeSuffix(CLASS_SUFFIX) + if (!jarFile.endsWith(".jar")) return null + val file = File(jarFile) + if (!file.exists()) return null + return getVersionsFromManifest(file) + } +} \ No newline at end of file diff --git a/plugins/kotlin-serialization/kotlin-serialization-compiler/test/org/jetbrains/kotlinx/serialization/RuntimeSearch.kt b/plugins/kotlin-serialization/kotlin-serialization-compiler/test/org/jetbrains/kotlinx/serialization/RuntimeSearch.kt index b4c0cc801bd..d6ad9ff8a14 100644 --- a/plugins/kotlin-serialization/kotlin-serialization-compiler/test/org/jetbrains/kotlinx/serialization/RuntimeSearch.kt +++ b/plugins/kotlin-serialization/kotlin-serialization-compiler/test/org/jetbrains/kotlinx/serialization/RuntimeSearch.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2020 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. */ @@ -7,8 +7,10 @@ package org.jetbrains.kotlinx.serialization import junit.framework.TestCase import org.jetbrains.kotlin.utils.PathUtil +import org.jetbrains.kotlinx.serialization.compiler.diagnostic.VersionReader import org.junit.Test import java.io.File +import kotlin.test.assertTrue class RuntimeLibraryInClasspathTest { private val runtimeLibraryPath = getSerializationLibraryRuntimeJar() @@ -20,6 +22,14 @@ class RuntimeLibraryInClasspathTest { runtimeLibraryPath ) } + + @Test + fun testRuntimeHasSufficientVersion() { + val version = VersionReader.getVersionsFromManifest(runtimeLibraryPath!!) + assertTrue(version.currentCompilerMatchRequired(), "Runtime version too high") + // todo: uncomment this when corresponding runtime version would be published +// assertTrue(version.implementationVersionMatchSupported(), "Runtime version too low") + } } internal fun getSerializationLibraryRuntimeJar(): File? = try { diff --git a/plugins/kotlin-serialization/kotlin-serialization-ide/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginIDEDeclarationChecker.kt b/plugins/kotlin-serialization/kotlin-serialization-ide/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginIDEDeclarationChecker.kt index e827c377983..86bb6b4f517 100644 --- a/plugins/kotlin-serialization/kotlin-serialization-ide/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginIDEDeclarationChecker.kt +++ b/plugins/kotlin-serialization/kotlin-serialization-ide/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginIDEDeclarationChecker.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2020 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. */ @@ -14,4 +14,7 @@ class SerializationPluginIDEDeclarationChecker : SerializationPluginDeclarationC // were imported into project model from Gradle. return getIfEnabledOn(descriptor) { true } == true } + + override val isIde: Boolean + get() = true } \ No newline at end of file