diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f203762..7ca2a83 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -2,6 +2,6 @@ \ No newline at end of file diff --git a/README.md b/README.md index 9d92d41..d4d04d8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ KMapper is a Kotlin compiler plugin that provides code generation capabilities f ## Features -- **Kotlin 2.0+ Support**: Built with K2 compiler support (Kotlin 2.3.10) +- **Kotlin 2.0+ Support**: Built with K2 compiler support (Kotlin 2.4.0) - **Fluent DSL**: Intuitive assignment-based mapping syntax with `property = value` - **Compile-time Validation**: Ensures all required constructor parameters are mapped - **IR-Based Generation**: Uses Kotlin's IR (Intermediate Representation) for robust code generation @@ -16,7 +16,7 @@ KMapper is a Kotlin compiler plugin that provides code generation capabilities f ## Requirements -- Kotlin 2.3.10 or later +- Kotlin 2.4.0 or later - JVM 17+ - Gradle build system @@ -29,7 +29,7 @@ Add the plugin to your project's `build.gradle.kts`: build.gradle.kts ```kotlin plugins { - kotlin("jvm") version "2.3.10" + kotlin("jvm") version "2.4.0" id("community.flock.kmapper") version "0.0.0-SNAPSHOT" } ``` @@ -52,7 +52,7 @@ Load the KMapper Maven integration by adding it as a dependency of kotlin-maven- - Auto-register the KMapper Kotlin compiler plugin (transitively on the plugin classpath) - Ensure the runtime library (compiler-runtime) is on your project compile classpath -Kotlin version used/tested: 2.3.10. +Kotlin version used/tested: 2.4.0. Minimal setup: @@ -74,7 +74,7 @@ Minimal setup: org.jetbrains.kotlin kotlin-maven-plugin - 2.3.10 + 2.4.0 ...(other plugin configuration) @@ -90,7 +90,7 @@ Minimal setup: Troubleshooting: -- Ensure kotlin-maven-plugin version is 2.3.10 (matching our tested Kotlin version). +- Ensure kotlin-maven-plugin version is 2.4.0 (matching our tested Kotlin version). - Make sure the KMapper maven-plugin dependency is placed under kotlin-maven-plugin’s (not in the project section). - In multi-module builds, add the kotlin-maven-plugin configuration in each module that compiles Kotlin (you can use in the parent for reuse). - You can set a property 0.0.0-SNAPSHOT and the extension will use it to resolve the runtime version if needed. diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 1fc8187..420b7b2 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("jvm") version "2.3.20" - kotlin("plugin.allopen") version "2.3.20" + kotlin("jvm") version "2.4.0" + kotlin("plugin.allopen") version "2.4.0" id("org.jetbrains.kotlinx.benchmark") version "0.4.13" id("community.flock.kmapper") version "0.0.0-SNAPSHOT" application diff --git a/build.gradle.kts b/build.gradle.kts index e21f873..202443b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "2.3.20" + kotlin("jvm") version "2.4.0" id("io.github.gradle-nexus.publish-plugin") version "2.0.0" id("org.jetbrains.dokka") version "2.0.0" apply false } diff --git a/compiler-plugin/build.gradle.kts b/compiler-plugin/build.gradle.kts index b81fdf1..a1e167a 100644 --- a/compiler-plugin/build.gradle.kts +++ b/compiler-plugin/build.gradle.kts @@ -1,8 +1,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "2.3.20" + kotlin("jvm") version "2.4.0" id("maven-publish") id("org.jetbrains.dokka") id("com.gradleup.kctf").version("2.3.10-0.0.2-SNAPSHOT-a524b7d38d0ad625c3b891df859cc0be4b9c339b") @@ -22,7 +23,14 @@ val kMapperRuntimeClasspath: Configuration by configurations.creating { dependencies { compileOnly(kotlin("compiler-embeddable")) - testImplementation("com.gradleup.kctf:kctf-runtime:2.3.10-0.0.2-SNAPSHOT-a524b7d38d0ad625c3b891df859cc0be4b9c339b") + // kctf-runtime is not published for Kotlin 2.4 yet, so the compiler test + // framework is depended on directly and kctf's classpath-based stdlib path + // provider is vendored in src/test/kotlin/kctf. + testImplementation(kotlin("compiler", getKotlinPluginVersion())) + testImplementation(kotlin("compiler-internal-test-framework", getKotlinPluginVersion())) + testRuntimeOnly(kotlin("reflect", getKotlinPluginVersion())) + testRuntimeOnly(kotlin("script-runtime", getKotlinPluginVersion())) + testRuntimeOnly(kotlin("annotations-jvm", getKotlinPluginVersion())) testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation(project(":compiler-runtime")) add(kMapperRuntimeClasspath.name, project(":compiler-runtime")) @@ -34,7 +42,6 @@ kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } - compilerOptions.freeCompilerArgs.add("-Xcontext-parameters") } val javadocJar by tasks.registering(Jar::class) { diff --git a/compiler-plugin/src/main/kotlin/community/flock/kmapper/compiler/KMapperExtension.kt b/compiler-plugin/src/main/kotlin/community/flock/kmapper/compiler/KMapperExtension.kt index 76a9b1e..4a8ffa8 100644 --- a/compiler-plugin/src/main/kotlin/community/flock/kmapper/compiler/KMapperExtension.kt +++ b/compiler-plugin/src/main/kotlin/community/flock/kmapper/compiler/KMapperExtension.kt @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.util.patchDeclarationParents import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid /** @@ -15,5 +16,9 @@ import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid class KMapperExtension(val collector: MessageCollector) : IrGenerationExtension { override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { moduleFragment.transformChildrenVoid(KMapperIrBuildMapperVisitor(pluginContext, collector)) + // Declarations synthesized by the visitor (e.g. the lambdas built for + // List mappings) are created detached; Kotlin >= 2.4 fails IR lowering + // when it encounters a declaration without an initialized parent. + moduleFragment.patchDeclarationParents() } } diff --git a/compiler-plugin/src/test/kotlin/kctf/ClasspathBasedStandardLibrariesPathProvider.kt b/compiler-plugin/src/test/kotlin/kctf/ClasspathBasedStandardLibrariesPathProvider.kt new file mode 100644 index 0000000..031d09f --- /dev/null +++ b/compiler-plugin/src/test/kotlin/kctf/ClasspathBasedStandardLibrariesPathProvider.kt @@ -0,0 +1,75 @@ +// Vendored from GradleUp/kctf (MIT), adapted to the Kotlin 2.4 test framework: +// KotlinStandardLibrariesPathProvider is an interface since 2.4 and kctf-runtime +// is not published for Kotlin 2.4 yet. The `kctf` package is kept so the +// kctf-generated test sources keep referring to this provider. +package kctf + +import org.jetbrains.kotlin.platform.wasm.WasmTarget +import org.jetbrains.kotlin.test.services.KotlinStandardLibrariesPathProvider +import java.io.File + +object ClasspathBasedStandardLibrariesPathProvider : KotlinStandardLibrariesPathProvider { + private val SEP = "\\${File.separator}" + + private val GRADLE_DEPENDENCY = + (".*?" + + SEP + + "(?[^$SEP]*)" + + SEP + + "(?[^$SEP]*)" + + SEP + + "[^$SEP]*" + + SEP + + "\\1-\\2\\.jar") + .toRegex() + + private val jars = + System.getProperty("java.class.path") + .split("\\${File.pathSeparator}".toRegex()) + .dropLastWhile(String::isEmpty) + .map(::File) + .associateBy { + GRADLE_DEPENDENCY.matchEntire(it.path)?.let { it.groups["name"]!!.value } ?: it.name + } + + private fun getFile(name: String): File { + return jars[name] + ?: error("Jar $name not found in classpath:\n${jars.entries.joinToString("\n")}") + } + + override fun runtimeJarForTests(): File = getFile("kotlin-stdlib") + + override fun runtimeJarForTestsWithJdk8(): File = getFile("kotlin-stdlib-jdk8") + + override fun minimalRuntimeJarForTests(): File = getFile("kotlin-stdlib") + + override fun reflectJarForTests(): File = getFile("kotlin-reflect") + + override fun kotlinTestJarForTests(): File = getFile("kotlin-test") + + override fun scriptRuntimeJarForTests(): File = getFile("kotlin-script-runtime") + + override fun jvmAnnotationsForTests(): File = getFile("kotlin-annotations-jvm") + + override fun getAnnotationsJar(): File = getFile("kotlin-annotations-jvm") + + override fun fullJsStdlib(): File = getFile("kotlin-stdlib-js") + + override fun defaultJsStdlib(): File = getFile("kotlin-stdlib-js") + + override fun kotlinTestJsKLib(): File = getFile("kotlin-test-js") + + override fun fullWasmStdlib(target: WasmTarget): File = TODO("Wasm is not supported by these tests") + + override fun kotlinTestWasmKLib(target: WasmTarget): File = TODO("Wasm is not supported by these tests") + + override fun webStdlibForTests(): File = TODO("Web is not supported by these tests") + + override fun commonStdlibForTests(): File { + TODO("Not yet implemented") + } + + override fun scriptingPluginFilesForTests(): Collection { + TODO("KT-67573") + } +} diff --git a/compiler-runtime/build.gradle.kts b/compiler-runtime/build.gradle.kts index 5d83779..b5b0d95 100644 --- a/compiler-runtime/build.gradle.kts +++ b/compiler-runtime/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - kotlin("jvm") version "2.3.20" + kotlin("jvm") version "2.4.0" id("maven-publish") id("org.jetbrains.dokka") signing diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index 1de71e2..0afda4b 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -26,7 +26,11 @@ sourceSets { } dependencies { - implementation(kotlin("gradle-plugin-api")) + // compileOnly so the consumer project's own Kotlin Gradle Plugin provides + // this API at runtime. With implementation scope the published POM would + // drag this version of KGP onto the consumer's plugin classpath, silently + // overriding the Kotlin version the project asked for. + compileOnly(kotlin("gradle-plugin-api")) testImplementation(kotlin("test-junit5")) } diff --git a/gradle-plugin/src/community/flock/kmapper/gradle/plugin/KMapperGradlePlugin.kt b/gradle-plugin/src/community/flock/kmapper/gradle/plugin/KMapperGradlePlugin.kt index 8fbbc70..7628c2f 100644 --- a/gradle-plugin/src/community/flock/kmapper/gradle/plugin/KMapperGradlePlugin.kt +++ b/gradle-plugin/src/community/flock/kmapper/gradle/plugin/KMapperGradlePlugin.kt @@ -2,8 +2,10 @@ package community.flock.kmapper.gradle.plugin import community.flock.kmapper.BuildConfig import community.flock.kmapper.BuildConfig.ANNOTATIONS_LIBRARY_COORDINATES +import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact @@ -11,8 +13,32 @@ import org.jetbrains.kotlin.gradle.plugin.SubpluginOption @Suppress("unused") // Used via reflection. class KMapperGradlePlugin : KotlinCompilerPluginSupportPlugin { + + companion object { + // The compiler plugin is built against the Kotlin 2.4 compiler API and + // cannot be loaded by older compilers; fail fast with a clear message + // instead of crashing compilation (see issue #31 for the 2.3-on-2.4 + // counterpart of that crash). + private val MINIMUM_KOTLIN_VERSION = KotlinVersion(2, 4, 0) + } + override fun apply(target: Project) { target.extensions.create("flockPlugin", KMapperGradleExtension::class.java) + target.plugins.withType(KotlinBasePlugin::class.java).configureEach { kotlinPlugin -> + checkKotlinVersion(kotlinPlugin.pluginVersion) + } + } + + private fun checkKotlinVersion(version: String) { + val parts = version.substringBefore('-').split('.').map { it.toIntOrNull() ?: return } + if (parts.size < 3) return + if (KotlinVersion(parts[0], parts[1], parts[2]) < MINIMUM_KOTLIN_VERSION) { + throw GradleException( + "kmapper requires Kotlin $MINIMUM_KOTLIN_VERSION or newer, but this project uses " + + "Kotlin $version. Older compilers cannot load the kmapper compiler plugin; " + + "either upgrade Kotlin or use a kmapper release built for Kotlin $version." + ) + } } override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true diff --git a/test-integration/build.gradle.kts b/test-integration/build.gradle.kts index bcefcd9..c14a698 100644 --- a/test-integration/build.gradle.kts +++ b/test-integration/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "2.3.20" + kotlin("jvm") version "2.4.0" } group = rootProject.group @@ -10,7 +10,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") implementation(gradleTestKit()) - implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.3.20") + implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0") } kotlin { diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/BasicMappingTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/BasicMappingTest.kt index d71665f..04bd79e 100644 --- a/test-integration/src/test/kotlin/community/flock/kmapper/BasicMappingTest.kt +++ b/test-integration/src/test/kotlin/community/flock/kmapper/BasicMappingTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test class BasicMappingTest { val options = IntegrationTest.Options( - kotlinVersion = "2.3.10", + kotlinVersion = "2.4.0", ) @Test @@ -579,8 +579,8 @@ class BasicMappingTest { """ |plugins { | id("community.flock.kmapper") version "0.0.0-SNAPSHOT" - | kotlin("jvm") version "2.3.10" - | kotlin("plugin.serialization") version "2.3.10" + | kotlin("jvm") version "2.4.0" + | kotlin("plugin.serialization") version "2.4.0" | application |} |repositories { diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/EnumMappingTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/EnumMappingTest.kt index ba387c3..df3a1a3 100644 --- a/test-integration/src/test/kotlin/community/flock/kmapper/EnumMappingTest.kt +++ b/test-integration/src/test/kotlin/community/flock/kmapper/EnumMappingTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test class EnumMappingTest { val options = IntegrationTest.Options( - kotlinVersion = "2.3.10", + kotlinVersion = "2.4.0", ) @Test diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/KotlinVersionGuardTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/KotlinVersionGuardTest.kt new file mode 100644 index 0000000..2ede1fc --- /dev/null +++ b/test-integration/src/test/kotlin/community/flock/kmapper/KotlinVersionGuardTest.kt @@ -0,0 +1,32 @@ +package community.flock.kmapper + +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class KotlinVersionGuardTest { + + val options = IntegrationTest.Options( + kotlinVersion = "2.3.21", + ) + + @Test + fun shouldFailFast_onKotlinOlderThanMinimum() { + IntegrationTest(options) + .file("App.kt") { + """ + |package sample + | + |fun main() { + | println("should never compile") + |} + | + """.trimMargin() + } + .compileFail { output -> + assertTrue( + output.contains("kmapper requires Kotlin 2.4.0 or newer"), + "Expected a clear minimum-Kotlin-version error, got:\n$output" + ) + } + } +} diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/ListMappingTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/ListMappingTest.kt index 3340be6..726bd99 100644 --- a/test-integration/src/test/kotlin/community/flock/kmapper/ListMappingTest.kt +++ b/test-integration/src/test/kotlin/community/flock/kmapper/ListMappingTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test class ListMappingTest { val options = IntegrationTest.Options( - kotlinVersion = "2.3.10", + kotlinVersion = "2.4.0", ) @Test diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/NullableAndDefaultsTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/NullableAndDefaultsTest.kt index f7c3a6a..5dc1949 100644 --- a/test-integration/src/test/kotlin/community/flock/kmapper/NullableAndDefaultsTest.kt +++ b/test-integration/src/test/kotlin/community/flock/kmapper/NullableAndDefaultsTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test class NullableAndDefaultsTest { val options = IntegrationTest.Options( - kotlinVersion = "2.3.10", + kotlinVersion = "2.4.0", ) @Test diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/NumericWideningTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/NumericWideningTest.kt index f6efe49..ca8a950 100644 --- a/test-integration/src/test/kotlin/community/flock/kmapper/NumericWideningTest.kt +++ b/test-integration/src/test/kotlin/community/flock/kmapper/NumericWideningTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test class NumericWideningTest { val options = IntegrationTest.Options( - kotlinVersion = "2.3.10", + kotlinVersion = "2.4.0", ) @Test diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt index 52c38f7..e47f29d 100644 --- a/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt +++ b/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt @@ -6,8 +6,8 @@ import org.junit.jupiter.api.Test class SerializableTest { val options = IntegrationTest.Options( - kotlinVersion = "2.3.10", - additionalPlugins = listOf("""kotlin("plugin.serialization") version "2.3.10""""), + kotlinVersion = "2.4.0", + additionalPlugins = listOf("""kotlin("plugin.serialization") version "2.4.0""""), additionalDependencies = listOf("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1"), ) diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/ValueClassTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/ValueClassTest.kt index b64d5e4..9bc29f8 100644 --- a/test-integration/src/test/kotlin/community/flock/kmapper/ValueClassTest.kt +++ b/test-integration/src/test/kotlin/community/flock/kmapper/ValueClassTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test class ValueClassTest { val options = IntegrationTest.Options( - kotlinVersion = "2.3.10", + kotlinVersion = "2.4.0", ) @Test