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.
This commit is contained in:
Leonid Startsev
2020-07-16 22:10:14 +03:00
parent 7db0bf8195
commit df5f38fec8
6 changed files with 146 additions and 9 deletions
@@ -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<PsiElement> INCORRECT_TRANSIENT = DiagnosticFactory0.create(WARNING);
DiagnosticFactory3<KtAnnotationEntry, String, String, String> REQUIRED_KOTLIN_TOO_HIGH = DiagnosticFactory3.create(ERROR);
DiagnosticFactory3<KtAnnotationEntry, String, String, String> PROVIDED_RUNTIME_TOO_LOW = DiagnosticFactory3.create(ERROR);
@SuppressWarnings("UnusedDeclaration")
Object _initializer = new Object() {
{
@@ -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<SerializableProperty>,
@@ -127,7 +172,7 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker {
val namesSet = mutableSetOf<String>()
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<in KtAnnotationEntry>) {
descriptor.safeReport { e ->
descriptor.onSerializableAnnotation { e ->
report(error.on(e))
}
}
@@ -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
)
}
}
@@ -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<ModuleDescriptor, RuntimeVersions> = 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)
}
}
@@ -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 {
@@ -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
}