diff --git a/.github/workflows/lint-and-test-dev.yml b/.github/workflows/lint-and-test-dev.yml index f3df20de..479d8a24 100644 --- a/.github/workflows/lint-and-test-dev.yml +++ b/.github/workflows/lint-and-test-dev.yml @@ -28,6 +28,10 @@ on: - "**.kt" - "**.java" - .github/workflows/lint-and-test-dev.yml + +permissions: + contents: write # for Dependency submission + jobs: lint-and-test-dev: name: lint-and-test-dev @@ -39,20 +43,18 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 - name: Set up java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '8' distribution: 'corretto' - - name: Lint - uses: gradle/gradle-build-action@v2 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 with: - arguments: lintKotlin + dependency-graph: generate-and-submit + dependency-graph-continue-on-failure: false + - name: Lint + run: ./gradlew lintKotlin - name: Test - uses: gradle/gradle-build-action@v2 - with: - arguments: test - # TODO: Prepare a separate WF to be executed only when there is a change in build.gradle. - - name: Run snapshot action - uses: mikepenz/gradle-dependency-submission@v0.9.1 + run: ./gradlew lintKotlin test diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8d064666..3118b715 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,3 +1,4 @@ +name: release on: workflow_dispatch: jobs: diff --git a/.github/workflows/test-main.yml b/.github/workflows/test-main.yml index 26cfd988..850f48e8 100644 --- a/.github/workflows/test-main.yml +++ b/.github/workflows/test-main.yml @@ -47,11 +47,11 @@ jobs: - name: '1.9.21 K2' version: '1.9.21' k2: true - - name: '2.0.0-Beta1' - version: '2.0.0-Beta1' + - name: '2.0.0-Beta3' + version: '2.0.0-Beta3' k2: false - - name: '2.0.0-Beta1 K2' - version: '2.0.0-Beta1' + - name: '2.0.0-Beta3 K2' + version: '2.0.0-Beta3' k2: true env: KOTLIN_VERSION: ${{ matrix.kotlin.version }} @@ -63,13 +63,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 - name: 'Set up java ${{ matrix.java-version }}' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '${{ matrix.java-version }}' distribution: 'corretto' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: Test - uses: gradle/gradle-build-action@v2 - with: - arguments: test + run: ./gradlew test diff --git a/build.gradle.kts b/build.gradle.kts index 4d4eecb3..7000ab8f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ val jacksonVersion = libs.versions.jackson.get() val generatedSrcPath = "${layout.buildDirectory.get()}/generated/kotlin" group = groupStr -version = "${jacksonVersion}-beta10" +version = "${jacksonVersion}-beta11" repositories { mavenCentral() @@ -98,6 +98,11 @@ public val kogeraVersion: Version = VersionUtil.parseVersion("$version", "$group into(file("$generatedSrcPath/${packageStr.replace(".", "/")}")) } + // Added to avoid failure in generating dependency graphs in CI. + lintKotlinMain { + dependsOn.add(generateKogeraVersion) + } + compileKotlin { dependsOn.add(generateKogeraVersion) kotlinOptions.jvmTarget = "1.8" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1578720a..e730c845 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ junit = "5.10.1" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" } -kotlinx-metadata-jvm = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.7.0" +kotlinx-metadata-jvm = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.9.0" jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt index dd28bf91..63f29a15 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt @@ -1,10 +1,12 @@ package io.github.projectmapk.jackson.module.kogera import com.fasterxml.jackson.annotation.JsonCreator +import kotlinx.metadata.KmClass import kotlinx.metadata.KmClassifier import kotlinx.metadata.KmType import kotlinx.metadata.isNullable import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.KotlinClassMetadata import java.lang.reflect.AnnotatedElement import java.lang.reflect.Constructor import java.lang.reflect.Method @@ -12,6 +14,10 @@ import java.lang.reflect.Method internal typealias JavaDuration = java.time.Duration internal typealias KotlinDuration = kotlin.time.Duration +internal fun Class<*>.toKmClass(): KmClass? = getAnnotation(Metadata::class.java)?.let { + (KotlinClassMetadata.readStrict(it) as KotlinClassMetadata.Class).kmClass +} + internal fun Class<*>.isUnboxableValueClass() = this.isAnnotationPresent(JvmInline::class.java) // JmClass must be value class. diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/JmClass.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/JmClass.kt index dad46bb3..0bceedcb 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/JmClass.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/JmClass.kt @@ -1,88 +1,29 @@ +// Visitor API has already been deprecated, but the error is being suppressed for now. +@file:Suppress("DEPRECATION_ERROR") + package io.github.projectmapk.jackson.module.kogera +import kotlinx.metadata.ClassKind import kotlinx.metadata.ClassName -import kotlinx.metadata.ExperimentalContextReceivers -import kotlinx.metadata.Flags -import kotlinx.metadata.KmClassExtensionVisitor -import kotlinx.metadata.KmClassVisitor +import kotlinx.metadata.KmClass import kotlinx.metadata.KmConstructor -import kotlinx.metadata.KmConstructorVisitor -import kotlinx.metadata.KmExtensionType import kotlinx.metadata.KmFunction -import kotlinx.metadata.KmFunctionVisitor import kotlinx.metadata.KmProperty -import kotlinx.metadata.KmPropertyVisitor import kotlinx.metadata.KmType -import kotlinx.metadata.KmTypeAliasVisitor -import kotlinx.metadata.KmTypeParameterVisitor -import kotlinx.metadata.KmTypeVisitor -import kotlinx.metadata.KmVariance -import kotlinx.metadata.KmVersionRequirementVisitor -import kotlinx.metadata.flagsOf -import kotlinx.metadata.internal.accept -import kotlinx.metadata.internal.metadata.jvm.deserialization.JvmProtoBufUtil import kotlinx.metadata.jvm.getterSignature import kotlinx.metadata.jvm.signature import java.lang.reflect.Constructor import java.lang.reflect.Field import java.lang.reflect.Method - -// KmClassVisitor with all processing disabled as much as possible to reduce load -internal sealed class ReducedKmClassVisitor : KmClassVisitor() { - final override val delegate: KmClassVisitor? get() = null - - // from KmDeclarationContainerVisitor - override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor? = null - override fun visitProperty( - flags: Flags, - name: String, - getterFlags: Flags, - setterFlags: Flags - ): KmPropertyVisitor? = null - override fun visitTypeAlias(flags: Flags, name: String): KmTypeAliasVisitor? = null - override fun visitExtensions(type: KmExtensionType): KmClassExtensionVisitor? = null - - // from KmClassVisitor - override fun visit(flags: Flags, name: ClassName) {} - override fun visitTypeParameter( - flags: Flags, - name: String, - id: Int, - variance: KmVariance - ): KmTypeParameterVisitor? = null - override fun visitSupertype(flags: Flags): KmTypeVisitor? = null - override fun visitConstructor(flags: Flags): KmConstructorVisitor? = null - override fun visitCompanionObject(name: String) {} - override fun visitNestedClass(name: String) {} - override fun visitEnumEntry(name: String) {} - override fun visitSealedSubclass(name: ClassName) {} - override fun visitInlineClassUnderlyingPropertyName(name: String) {} - override fun visitInlineClassUnderlyingType(flags: Flags): KmTypeVisitor? = null - - @OptIn(ExperimentalContextReceivers::class) - override fun visitContextReceiverType(flags: Flags): KmTypeVisitor? = null - override fun visitVersionRequirement(): KmVersionRequirementVisitor? = null - override fun visitEnd() {} -} +import kotlinx.metadata.internal.metadata.deserialization.Flags as ProtoFlags // Jackson Metadata Class internal sealed interface JmClass { class CompanionObject(declaringClass: Class<*>, companionObject: String) { - private class ReducedCompanionVisitor(companionClass: Class<*>) : ReducedKmClassVisitor() { - val functions: MutableList = arrayListOf() - - init { - companionClass.getAnnotation(Metadata::class.java)!!.accept(this) - } - - override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor = KmFunction(flags, name) - .apply { functions.add(this) } - } - private val companionField: Field = declaringClass.getDeclaredField(companionObject) val type: Class<*> = companionField.type val isAccessible: Boolean = companionField.isAccessible - private val functions by lazy { ReducedCompanionVisitor(type).functions } + private val functions by lazy { type.toKmClass()!!.functions } val instance: Any by lazy { // To prevent the call from failing, save the initial value and then rewrite the flag. if (!companionField.isAccessible) companionField.isAccessible = true @@ -95,7 +36,7 @@ internal sealed interface JmClass { } } - val flags: Flags + val kind: ClassKind val constructors: List val sealedSubclasses: List val inlineClassUnderlyingType: KmType? @@ -110,34 +51,35 @@ internal sealed interface JmClass { private class JmClassImpl( clazz: Class<*>, - metadata: Metadata, + kmClass: KmClass, superJmClass: JmClass?, interfaceJmClasses: List -) : ReducedKmClassVisitor(), JmClass { - private val allPropsMap: MutableMap = mutableMapOf() +) : JmClass { + private val allPropsMap: Map // Defined as non-lazy because it is always read in both serialization and deserialization override val properties: List - private var companionPropName: String? = null - override var flags: Flags = flagsOf() - override val constructors: MutableList = mutableListOf() - override val sealedSubclasses: MutableList = mutableListOf() - override var inlineClassUnderlyingType: KmType? = null + private val companionPropName: String? = kmClass.companionObject + override val kind: ClassKind = ClassKind.values()[ProtoFlags.CLASS_KIND.get(kmClass.flags).number] + override val constructors: List = kmClass.constructors + override val sealedSubclasses: List = kmClass.sealedSubclasses + override val inlineClassUnderlyingType: KmType? = kmClass.inlineClassUnderlyingType init { - metadata.accept(this) - // Add properties of inherited classes and interfaces // If an `interface` is implicitly implemented by an abstract class, // it is necessary to obtain a more specific type, so always add it from the abstract class first. - (superJmClass as JmClassImpl?)?.allPropsMap?.forEach { - this.allPropsMap.putIfAbsent(it.key, it.value) + val tempPropsMap = ((superJmClass as JmClassImpl?)?.allPropsMap?.toMutableMap() ?: mutableMapOf()).apply { + kmClass.properties.forEach { + this[it.name] = it + } } - @Suppress("UNCHECKED_CAST") - (interfaceJmClasses as List).forEach { i -> - i.allPropsMap.forEach { - this.allPropsMap.putIfAbsent(it.key, it.value) + + allPropsMap = interfaceJmClasses.fold(tempPropsMap) { acc, cur -> + val curProps = (cur as JmClassImpl).allPropsMap + acc.apply { + curProps.forEach { acc.putIfAbsent(it.key, it.value) } } } @@ -178,38 +120,11 @@ private class JmClassImpl( val getterName = getter.name return properties.find { it.getterSignature?.name == getterName } } - - // KmClassVisitor - override fun visit(flags: Flags, name: ClassName) { - this.flags = flags - } - - override fun visitProperty(flags: Flags, name: String, getterFlags: Flags, setterFlags: Flags): KmPropertyVisitor = - KmProperty(flags, name, getterFlags, setterFlags).apply { allPropsMap[name] = this } - - override fun visitConstructor(flags: Flags): KmConstructorVisitor = - KmConstructor(flags).apply { constructors.add(this) } - - override fun visitCompanionObject(name: String) { - this.companionPropName = name - } - - override fun visitSealedSubclass(name: ClassName) { - sealedSubclasses.add(name) - } - - override fun visitInlineClassUnderlyingType(flags: Flags): KmTypeVisitor = - KmType(flags).also { inlineClassUnderlyingType = it } -} - -private fun Metadata.accept(visitor: ReducedKmClassVisitor) { - val (strings, proto) = JvmProtoBufUtil.readClassDataFrom(data1.takeIf(Array<*>::isNotEmpty)!!, data2) - proto.accept(visitor, strings) } internal fun JmClass( clazz: Class<*>, - metadata: Metadata, + kmClass: KmClass, superJmClass: JmClass?, interfaceJmClasses: List -): JmClass = JmClassImpl(clazz, metadata, superJmClass, interfaceJmClasses) +): JmClass = JmClassImpl(clazz, kmClass, superJmClass, interfaceJmClasses) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt index 36cc234a..4ef0d005 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt @@ -48,7 +48,7 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria fun getJmClass(clazz: Class<*>): JmClass? { return find(clazz) ?: run { - val metadata = clazz.getAnnotation(Metadata::class.java) ?: return null + val kmClass = clazz.toKmClass() ?: return null // Do not parse super class for interfaces. val superJmClass = if (!clazz.isInterface) { @@ -61,7 +61,7 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria } val interfaceJmClasses = clazz.interfaces.mapNotNull { getJmClass(it) } - val value = JmClass(clazz, metadata, superJmClass, interfaceJmClasses) + val value = JmClass(clazz, kmClass, superJmClass, interfaceJmClasses) putIfAbsent(clazz, value) } } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/singletonSupport/KotlinBeanDeserializerModifier.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/singletonSupport/KotlinBeanDeserializerModifier.kt index db2d9d66..8eb7ef6a 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/singletonSupport/KotlinBeanDeserializerModifier.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/singletonSupport/KotlinBeanDeserializerModifier.kt @@ -5,7 +5,7 @@ import com.fasterxml.jackson.databind.DeserializationConfig import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier import io.github.projectmapk.jackson.module.kogera.ReflectionCache -import kotlinx.metadata.Flag +import kotlinx.metadata.ClassKind import java.io.Serializable // [module-kotlin#225]: keep Kotlin singletons as singletons @@ -19,10 +19,8 @@ internal class KotlinBeanDeserializerModifier( } private fun objectSingletonInstance(beanClass: Class<*>): Any? = cache.getJmClass(beanClass)?.let { - val flags = it.flags - // It is not assumed that the companion object is the target - if (Flag.Class.IS_OBJECT(flags) && !Flag.Class.IS_COMPANION_OBJECT(flags)) { + if (it.kind == ClassKind.OBJECT) { beanClass.getDeclaredField("INSTANCE").get(null) } else { null