diff --git a/libraries/tools/kotlin-tooling-metadata/src/main/kotlin/org/jetbrains/kotlin/tooling/JsonConversion.kt b/libraries/tools/kotlin-tooling-metadata/src/main/kotlin/org/jetbrains/kotlin/tooling/JsonConversion.kt index 7459455974e..f6144cba493 100644 --- a/libraries/tools/kotlin-tooling-metadata/src/main/kotlin/org/jetbrains/kotlin/tooling/JsonConversion.kt +++ b/libraries/tools/kotlin-tooling-metadata/src/main/kotlin/org/jetbrains/kotlin/tooling/JsonConversion.kt @@ -118,8 +118,14 @@ fun KotlinToolingMetadata.Companion.parseJsonOrThrow(value: String): KotlinTooli } private fun JsonObject.toKotlinToolingMetadataOrThrow(): KotlinToolingMetadata { + val schemaVersion = SchemaVersion.parseStringOrThrow(getOrThrow("schemaVersion").asString) + if (!SchemaVersion.current.isCompatible(schemaVersion)) { + throw IllegalArgumentException( + "Incompatible schemaVersion='$schemaVersion' found. Current schemaVersion='${SchemaVersion.current}'" + ) + } return KotlinToolingMetadata( - schemaVersion = getOrThrow("schemaVersion").asString, + schemaVersion = schemaVersion.toString(), buildSystem = getOrThrow("buildSystem").asString, buildSystemVersion = getOrThrow("buildSystemVersion").asString, buildPlugin = getOrThrow("buildPlugin").asString, diff --git a/libraries/tools/kotlin-tooling-metadata/src/main/kotlin/org/jetbrains/kotlin/tooling/SchemaVersion.kt b/libraries/tools/kotlin-tooling-metadata/src/main/kotlin/org/jetbrains/kotlin/tooling/SchemaVersion.kt new file mode 100644 index 00000000000..19583a8928c --- /dev/null +++ b/libraries/tools/kotlin-tooling-metadata/src/main/kotlin/org/jetbrains/kotlin/tooling/SchemaVersion.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2021 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.tooling + +internal data class SchemaVersion(val major: Int, val minor: Int, val patch: Int) : Comparable { + override fun compareTo(other: SchemaVersion): Int { + (this.major - other.major).takeIf { it != 0 }?.let { return it } + (this.minor - other.minor).takeIf { it != 0 }?.let { return it } + return this.patch - other.patch + } + + override fun toString(): String { + return "$major.$minor.$patch" + } + + companion object { + val current by lazy { SchemaVersion.parseStringOrThrow(KotlinToolingMetadata.currentSchemaVersion) } + } +} + +internal fun SchemaVersion.Companion.parseStringOrThrow(schemaVersion: String): SchemaVersion { + fun throwIllegalSchemaVersion(): Nothing = throw IllegalArgumentException( + "Illegal schemaVersion $schemaVersion. Expected {major}.{minor}.{patch}" + ) + + val parts = schemaVersion.split(".") + if (parts.size != 3) throwIllegalSchemaVersion() + + return SchemaVersion( + major = parts[0].toIntOrNull() ?: throwIllegalSchemaVersion(), + minor = parts[1].toIntOrNull() ?: throwIllegalSchemaVersion(), + patch = parts[2].toIntOrNull() ?: throwIllegalSchemaVersion() + ) +} + +internal fun SchemaVersion.isCompatible(to: SchemaVersion): Boolean { + if (this.major != to.major) { + return false + } + + return (this.minor >= to.minor) +} diff --git a/libraries/tools/kotlin-tooling-metadata/src/test/kotlin/org/jetbrains/kotlin/tooling/DeserializationFailureTest.kt b/libraries/tools/kotlin-tooling-metadata/src/test/kotlin/org/jetbrains/kotlin/tooling/DeserializationFailureTest.kt index c58e3fac7dc..c344e24ec40 100644 --- a/libraries/tools/kotlin-tooling-metadata/src/test/kotlin/org/jetbrains/kotlin/tooling/DeserializationFailureTest.kt +++ b/libraries/tools/kotlin-tooling-metadata/src/test/kotlin/org/jetbrains/kotlin/tooling/DeserializationFailureTest.kt @@ -24,10 +24,11 @@ class DeserializationFailureTest { } @Test - fun `sample2 missing build pluginVersion`() { + fun `sample2 missing buildPluginVersion`() { @Language("JSON") val json = """ { + "schemaVersion": ${SchemaVersion.current}, "buildSystem": "Gradle", "buildSystemVersion": "6.7", "buildPlugin": "org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper", @@ -45,6 +46,30 @@ class DeserializationFailureTest { "Expected parsing failure, because of missing pluginVersion. Actual: $result" ) - assertFailsWith { KotlinToolingMetadata.parseJsonOrThrow(json) } + val exception = assertFailsWith { KotlinToolingMetadata.parseJsonOrThrow(json) } + assertTrue( + "buildPluginVersion" in exception.message.orEmpty(), + "Expected 'buildPluginVersion' mentioned in error message\n${exception.message}" + ) + } + + @Test + fun `sample3 incompatible schemaVersion`() { + val majorUpgradeVersion = SchemaVersion.current.copy(major = SchemaVersion.current.major + 1) + val metadataString = KotlinToolingMetadata( + schemaVersion = majorUpgradeVersion.toString(), + buildSystem = "", + buildSystemVersion = "", + buildPlugin = "", + buildPluginVersion = "", + projectSettings = KotlinToolingMetadata.ProjectSettings(false, false), + projectTargets = emptyList() + ).toJsonString() + + val exception = assertFailsWith { KotlinToolingMetadata.parseJsonOrThrow(metadataString) } + assertTrue( + majorUpgradeVersion.toString() in exception.message.orEmpty(), + "Expected bad schemaVersion mentioned in error message\n${exception.message}" + ) } } diff --git a/libraries/tools/kotlin-tooling-metadata/src/test/kotlin/org/jetbrains/kotlin/tooling/SchemaVersionTest.kt b/libraries/tools/kotlin-tooling-metadata/src/test/kotlin/org/jetbrains/kotlin/tooling/SchemaVersionTest.kt new file mode 100644 index 00000000000..c482c2b24b3 --- /dev/null +++ b/libraries/tools/kotlin-tooling-metadata/src/test/kotlin/org/jetbrains/kotlin/tooling/SchemaVersionTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2021 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.tooling + +import kotlin.test.* + +class SchemaVersionTest { + + @Test + fun `compare same version`() { + assertTrue(SchemaVersion(1, 0, 0) <= SchemaVersion(1, 0, 0), "Expected 1.0.0 <= 1.0.0") + assertTrue(SchemaVersion(1, 0, 0) >= SchemaVersion(1, 0, 0), "Expected 1.0.0 >= 1.0.0") + assertFalse(SchemaVersion(1, 0, 0) > SchemaVersion(1, 0, 0), "*NOT* Expected 1.0.0 > 1.0.0") + } + + @Test + fun `compare higher version`() { + assertTrue(SchemaVersion(2, 2, 2) > SchemaVersion(2, 2, 1)) + assertTrue(SchemaVersion(2, 2, 2) > SchemaVersion(2, 1, 2)) + assertTrue(SchemaVersion(2, 2, 2) > SchemaVersion(1, 3, 3)) + } + + @Test + fun `compare lower version`() { + assertTrue(SchemaVersion(2, 2, 2) < SchemaVersion(2, 2, 3)) + assertTrue(SchemaVersion(2, 2, 2) < SchemaVersion(2, 3, 1)) + assertTrue(SchemaVersion(2, 2, 2) < SchemaVersion(3, 1, 1)) + } + + @Test + fun parseSchemaVersion() { + assertEquals( + SchemaVersion(2, 15, 100), SchemaVersion.parseStringOrThrow("2.15.100") + ) + + assertEquals( + SchemaVersion(0, 0, 0), SchemaVersion.parseStringOrThrow("0.0.0") + ) + + assertEquals( + SchemaVersion(1, 0, 0), SchemaVersion.parseStringOrThrow("1.0.0") + ) + } + + @Test + fun `parseSchemaVersion failure`() { + assertFailsWith { SchemaVersion.parseStringOrThrow("1.0") } + assertFailsWith { SchemaVersion.parseStringOrThrow("1.0.0.0") } + assertFailsWith { SchemaVersion.parseStringOrThrow("a.b.c") } + } + + @Test + fun `toString and parse`() { + assertEquals( + SchemaVersion(1, 0, 0), SchemaVersion.parseStringOrThrow(SchemaVersion(1, 0, 0).toString()) + ) + + assertEquals( + SchemaVersion(28, 100, 900), SchemaVersion.parseStringOrThrow(SchemaVersion(28, 100, 900).toString()) + ) + } + + @Test + fun isCompatible() { + assertTrue(SchemaVersion(1, 1, 0).isCompatible(SchemaVersion(1, 0, 0))) + assertTrue(SchemaVersion(1, 0, 0).isCompatible(SchemaVersion(1, 0, 0))) + assertFalse(SchemaVersion(1, 0, 0).isCompatible(SchemaVersion(1, 1, 0))) + assertFalse(SchemaVersion(1, 0, 0).isCompatible(SchemaVersion(2, 0, 0))) + assertFalse(SchemaVersion(2, 0, 0).isCompatible(SchemaVersion(1, 0, 0))) + } +}