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:
+4
-4
@@ -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() {
|
||||
{
|
||||
|
||||
+48
-3
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+62
@@ -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)
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -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 {
|
||||
|
||||
+4
-1
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user