diff --git a/.github/workflows/lint-and-test-dev.yml b/.github/workflows/lint-and-test-dev.yml index 479d8a24..6ff908bd 100644 --- a/.github/workflows/lint-and-test-dev.yml +++ b/.github/workflows/lint-and-test-dev.yml @@ -43,7 +43,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v2 + uses: gradle/actions/wrapper-validation@v3 - name: Set up java uses: actions/setup-java@v4 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3118b715..a57a014e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,7 +6,7 @@ jobs: name: git-pr-release runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: git-pr-release diff --git a/.github/workflows/test-main.yml b/.github/workflows/test-main.yml index 18dcbaa5..0449cd71 100644 --- a/.github/workflows/test-main.yml +++ b/.github/workflows/test-main.yml @@ -35,23 +35,27 @@ jobs: fail-fast: false matrix: # LTS versions, latest version (if exists) - java-version: [ '8', '11', '17', '21', '22' ] + java-version: [ '8', '11', '17', '21', '23' ] # Minimum version, latest release version, latest pre-release version (if exists) kotlin: - name: '1.8.22' version: '1.8.22' k2: false - - name: '1.9.23' - version: '1.9.23' + - name: '1.9.25' + version: '1.9.25' k2: false - - name: '1.9.23 K2' - version: '1.9.23' + - name: '1.9.25 K2' + version: '1.9.25' k2: true - - name: '2.0.0-Beta5' - version: '2.0.0-Beta5' + - name: '2.0.21' + version: '2.0.21' k2: false - - name: '2.0.0-Beta5 K2' - version: '2.0.0-Beta5' + - name: '2.0.21 K2' + version: '2.0.21' + k2: true + - name: '2.1.0-RC' + k2: false + - name: '2.1.0-RC K2' k2: true env: KOTLIN_VERSION: ${{ matrix.kotlin.version }} @@ -63,7 +67,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v2 + uses: gradle/actions/wrapper-validation@v3 - name: 'Set up java ${{ matrix.java-version }}' uses: actions/setup-java@v4 with: diff --git a/README.md b/README.md index 366998f4..b63ff580 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This project makes several disruptive changes to achieve more `Kotlin-like` beha Details are summarized in [KogeraSpecificImplementations](./docs/KogeraSpecificImplementations.md). # Compatibility -- `jackson 2.16.x` +- `jackson 2.17.x` - `Java 8+` - `Kotlin 1.8.22+` diff --git a/build.gradle.kts b/build.gradle.kts index 3efde605..a725d323 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}-beta13" +version = "${jacksonVersion}-beta14" repositories { mavenCentral() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4736f224..73e2b8ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] kotlin = "1.8.22" # Mainly for CI, it can be rewritten by environment variable. -jackson = "2.17.1" +jackson = "2.17.3" # test libs -junit = "5.10.2" +junit = "5.11.3" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" } @@ -16,7 +16,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } -mockk = "io.mockk:mockk:1.13.10" +mockk = "io.mockk:mockk:1.13.13" jackson-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml", version.ref = "jackson" } jackson-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c..a4b76b95 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e..df97d72b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -145,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +205,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..9d21a218 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail 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 2dd9297b..fcbd3887 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 @@ -6,7 +6,6 @@ import io.github.projectmapk.jackson.module.kogera.annotation.JsonKUnbox 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 @@ -22,9 +21,6 @@ internal fun Class<*>.toKmClass(): KmClass? = getAnnotation(METADATA_CLASS)?.let internal fun Class<*>.isUnboxableValueClass() = this.isAnnotationPresent(JVM_INLINE_CLASS) -// JmClass must be value class. -internal fun JmClass.wrapsNullValueClass() = inlineClassUnderlyingType!!.isNullable - private val primitiveClassToDesc = mapOf( Byte::class.java to 'B', Char::class.java to 'C', @@ -87,8 +83,9 @@ internal fun String.reconstructClass(): Class<*> { return Class.forName(String(replaced)) } -internal fun KmType.reconstructClassOrNull(): Class<*>? = (classifier as? KmClassifier.Class) - ?.let { kotlin.runCatching { it.name.reconstructClass() }.getOrNull() } +internal fun KmType.reconstructClassOrNull(): Class<*>? = (classifier as? KmClassifier.Class)?.reconstructClassOrNull() +internal fun KmClassifier.Class.reconstructClassOrNull(): Class<*>? = + runCatching { name.reconstructClass() }.getOrNull() internal fun AnnotatedElement.hasCreatorAnnotation(): Boolean = getAnnotation(JSON_CREATOR_CLASS) ?.let { it.mode != JsonCreator.Mode.DISABLED } 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 6e05d41c..f095307e 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 @@ -1,6 +1,7 @@ package io.github.projectmapk.jackson.module.kogera import com.fasterxml.jackson.databind.util.LRUMap +import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import java.io.Serializable import java.lang.reflect.Method import java.util.Optional diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt index 1cde0efa..43c4a06e 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt @@ -16,13 +16,11 @@ import io.github.projectmapk.jackson.module.kogera.JSON_K_UNBOX_CLASS import io.github.projectmapk.jackson.module.kogera.KOTLIN_DURATION_CLASS import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass -import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull +import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter import io.github.projectmapk.jackson.module.kogera.ser.KotlinDurationValueToJavaDurationConverter import io.github.projectmapk.jackson.module.kogera.ser.KotlinToJavaDurationConverter import io.github.projectmapk.jackson.module.kogera.ser.SequenceToIteratorConverter -import io.github.projectmapk.jackson.module.kogera.wrapsNullValueClass import kotlinx.metadata.KmTypeProjection -import kotlinx.metadata.KmValueParameter import kotlinx.metadata.isNullable import java.lang.reflect.Constructor import java.lang.reflect.Method @@ -36,9 +34,9 @@ internal class KotlinFallbackAnnotationIntrospector( private val useJavaDurationConversion: Boolean, private val cache: ReflectionCache ) : NopAnnotationIntrospector() { - private fun findKotlinParameter(param: AnnotatedParameter): KmValueParameter? = + private fun findKotlinParameter(param: AnnotatedParameter): JmValueParameter? = when (val owner = param.owner.member) { - is Constructor<*> -> cache.getJmClass(param.declaringClass)?.findKmConstructor(owner)?.valueParameters + is Constructor<*> -> cache.getJmClass(param.declaringClass)?.findJmConstructor(owner)?.valueParameters is Method -> if (Modifier.isStatic(owner.modifiers)) { cache.getJmClass(param.declaringClass) ?.companion @@ -49,7 +47,7 @@ internal class KotlinFallbackAnnotationIntrospector( else -> null }?.let { it[param.index] } - private fun findKotlinParameter(param: Annotated): KmValueParameter? = + private fun findKotlinParameter(param: Annotated): JmValueParameter? = (param as? AnnotatedParameter)?.let { findKotlinParameter(it) } // since 2.4 @@ -66,7 +64,7 @@ internal class KotlinFallbackAnnotationIntrospector( override fun refineDeserializationType(config: MapperConfig<*>, a: Annotated, baseType: JavaType): JavaType = findKotlinParameter(a)?.let { param -> val rawType = a.rawType - param.type.reconstructClassOrNull() + param.reconstructedClassOrNull ?.takeIf { it.isUnboxableValueClass() && it != rawType } ?.let { config.constructType(it) } } ?: baseType @@ -101,7 +99,7 @@ internal class KotlinFallbackAnnotationIntrospector( // Determine if the unbox result of value class is nullable // @see findNullSerializer - private fun Class<*>.requireRebox(): Boolean = cache.getJmClass(this)!!.wrapsNullValueClass() + private fun Class<*>.requireRebox(): Boolean = cache.getJmClass(this)!!.wrapsNullableIfValue // Perform proper serialization even if the value wrapped by the value class is null. // If value is a non-null object type, it must not be reboxing. @@ -124,11 +122,11 @@ internal class KotlinFallbackAnnotationIntrospector( ?: super.findSetterInfo(ann) } -private fun KmValueParameter.isNullishTypeAt(index: Int): Boolean = type.arguments.getOrNull(index)?.let { +private fun JmValueParameter.isNullishTypeAt(index: Int): Boolean = arguments.getOrNull(index)?.let { // If it is not a StarProjection, type is not null it === KmTypeProjection.STAR || it.type!!.isNullable } ?: true // If a type argument cannot be taken, treat it as nullable to avoid unexpected failure. -private fun KmValueParameter.requireStrictNullCheck(type: JavaType): Boolean = +private fun JmValueParameter.requireStrictNullCheck(type: JavaType): Boolean = ((type.isArrayType || type.isCollectionLikeType) && !this.isNullishTypeAt(0)) || (type.isMapLikeType && !this.isNullishTypeAt(1)) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt index 4a502ae3..d263e4af 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt @@ -13,20 +13,15 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedParameter import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector import com.fasterxml.jackson.databind.jsontype.NamedType import io.github.projectmapk.jackson.module.kogera.JSON_PROPERTY_CLASS -import io.github.projectmapk.jackson.module.kogera.JmClass import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation +import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass +import io.github.projectmapk.jackson.module.kogera.jmClass.JmProperty +import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter import io.github.projectmapk.jackson.module.kogera.reconstructClass import io.github.projectmapk.jackson.module.kogera.toSignature import kotlinx.metadata.KmClassifier -import kotlinx.metadata.KmProperty -import kotlinx.metadata.KmValueParameter -import kotlinx.metadata.declaresDefaultValue import kotlinx.metadata.isNullable -import kotlinx.metadata.isSecondary -import kotlinx.metadata.jvm.getterSignature -import kotlinx.metadata.jvm.setterSignature -import kotlinx.metadata.jvm.signature import java.lang.reflect.Constructor import java.lang.reflect.Executable import java.lang.reflect.Method @@ -71,11 +66,11 @@ internal class KotlinPrimaryAnnotationIntrospector( // only a check for the existence of a getter is performed. // https://youtrack.jetbrains.com/issue/KT-6519 ?.let { - if (it.getterSignature == null) !(it.returnType.isNullable || type.hasDefaultEmptyValue()) else null + if (it.getterName == null) !(it.returnType.isNullable || type.hasDefaultEmptyValue()) else null } } - private fun KmProperty.isRequiredByNullability(): Boolean = !this.returnType.isNullable + private fun JmProperty.isRequiredByNullability(): Boolean = !this.returnType.isNullable private fun AnnotatedMethod.getRequiredMarkerFromCorrespondingAccessor(jmClass: JmClass): Boolean? = when (parameterCount) { @@ -93,7 +88,7 @@ internal class KotlinPrimaryAnnotationIntrospector( private fun AnnotatedParameter.hasRequiredMarker(jmClass: JmClass): Boolean? { val paramDef = when (val member = member) { - is Constructor<*> -> jmClass.findKmConstructor(member)?.valueParameters + is Constructor<*> -> jmClass.findJmConstructor(member)?.valueParameters is Method -> jmClass.companion?.findFunctionByMethod(member)?.valueParameters else -> null }?.let { it[index] } ?: return null // Return null if function on Kotlin cannot be determined @@ -101,11 +96,11 @@ internal class KotlinPrimaryAnnotationIntrospector( // non required if... return when { // Argument definition is nullable - paramDef.type.isNullable -> false + paramDef.isNullable -> false // Default argument are defined - paramDef.declaresDefaultValue -> false + paramDef.isOptional -> false // vararg is treated as an empty array because undefined input is allowed - paramDef.varargElementType != null -> false + paramDef.isVararg -> false // The conversion in case of null is defined. type.hasDefaultEmptyValue() -> false else -> true @@ -141,18 +136,18 @@ internal class KotlinPrimaryAnnotationIntrospector( } } -private fun Constructor<*>.isPrimarilyConstructorOf(jmClass: JmClass): Boolean = jmClass.findKmConstructor(this) +private fun Constructor<*>.isPrimarilyConstructorOf(jmClass: JmClass): Boolean = jmClass.findJmConstructor(this) ?.let { !it.isSecondary || jmClass.constructors.size == 1 } ?: false private fun KmClassifier.isString(): Boolean = this is KmClassifier.Class && this.name == "kotlin/String" private fun isPossibleSingleString( - kotlinParams: List, + kotlinParams: List, javaFunction: Executable, propertyNames: Set ): Boolean = kotlinParams.size == 1 && - kotlinParams[0].let { it.name !in propertyNames && it.type.classifier.isString() } && + kotlinParams[0].let { it.name !in propertyNames && it.isString } && javaFunction.parameters[0].annotations.none { it is JsonProperty } private fun hasCreatorConstructor(clazz: Class<*>, jmClass: JmClass, propertyNames: Set): Boolean { diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index eea3df31..fe782bfa 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -9,7 +9,6 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.module.SimpleDeserializers -import io.github.projectmapk.jackson.module.kogera.JmClass import io.github.projectmapk.jackson.module.kogera.KotlinDuration import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter @@ -17,6 +16,7 @@ import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationCon import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass +import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import io.github.projectmapk.jackson.module.kogera.toSignature import kotlinx.metadata.isSecondary import kotlinx.metadata.jvm.signature diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt index d678a778..3c5aa633 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt @@ -18,7 +18,6 @@ import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClass import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ConstructorValueCreator import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.MethodValueCreator import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ValueCreator -import io.github.projectmapk.jackson.module.kogera.wrapsNullValueClass import java.lang.reflect.Constructor import java.lang.reflect.Executable import java.lang.reflect.Method @@ -48,7 +47,7 @@ internal class KotlinValueInstantiator( valueDeserializer: JsonDeserializer<*>? ): Boolean = !isNullableParam && valueDeserializer is WrapsNullableValueClassDeserializer<*> && - cache.getJmClass(valueDeserializer.handledType())!!.wrapsNullValueClass() + cache.getJmClass(valueDeserializer.handledType())!!.wrapsNullableIfValue private val valueCreator: ValueCreator<*>? by ReflectProperties.lazySoft { val creator = _withArgsCreator.annotated as Executable diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt index 10e2a143..c44d9a9d 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt @@ -2,7 +2,7 @@ package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argu import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.calcMaskSize -import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ValueParameter +import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter import java.lang.reflect.Array as ReflectArray private fun defaultPrimitiveValue(type: Class<*>): Any = when (type) { @@ -47,7 +47,7 @@ private fun IntArray.update(index: Int, operation: MaskOperation) { // @see https://github.com/JetBrains/kotlin/blob/4c925d05883a8073e6732bca95bf575beb031a59/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KCallableImpl.kt#L114 internal class BucketGenerator( parameterTypes: List>, - valueParameters: List, + valueParameters: List, private val converters: List?> ) { private val valueParameterSize: Int = parameterTypes.size diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ConstructorValueCreator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ConstructorValueCreator.kt index 7cc5ac59..dc4ea3ad 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ConstructorValueCreator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ConstructorValueCreator.kt @@ -1,6 +1,5 @@ package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator -import io.github.projectmapk.jackson.module.kogera.JmClass import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.call import io.github.projectmapk.jackson.module.kogera.defaultConstructorMarker @@ -8,6 +7,8 @@ import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argum import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.BucketGenerator import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.calcMaskSize import io.github.projectmapk.jackson.module.kogera.getDeclaredConstructorBy +import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass +import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter import java.lang.reflect.Constructor internal class ConstructorValueCreator( @@ -19,21 +20,19 @@ internal class ConstructorValueCreator( override val isAccessible: Boolean = constructor.isAccessible override val callableName: String = constructor.name - override val valueParameters: List + override val valueParameters: List override val bucketGenerator: BucketGenerator init { // To prevent the call from failing, save the initial value and then rewrite the flag. if (!isAccessible) constructor.isAccessible = true - val constructorParameters = declaringJmClass.findKmConstructor(constructor)!!.valueParameters - - valueParameters = constructorParameters.map { ValueParameter(it) } + valueParameters = declaringJmClass.findJmConstructor(constructor)!!.valueParameters val rawTypes = constructor.parameterTypes.asList() bucketGenerator = BucketGenerator( rawTypes, valueParameters, - constructorParameters.mapToConverters(rawTypes, cache) + valueParameters.mapToConverters(rawTypes, cache) ) } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt index c9a41290..781f1a2e 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt @@ -1,14 +1,14 @@ package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator import io.github.projectmapk.jackson.module.kogera.ANY_CLASS -import io.github.projectmapk.jackson.module.kogera.JmClass import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.call import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.ArgumentBucket import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.BucketGenerator import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.calcMaskSize import io.github.projectmapk.jackson.module.kogera.getDeclaredMethodBy -import kotlinx.metadata.KmFunction +import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass +import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter import java.lang.reflect.Method internal class MethodValueCreator( @@ -19,22 +19,21 @@ internal class MethodValueCreator( private val companion: JmClass.CompanionObject = declaringJmClass.companion!! override val isAccessible: Boolean = method.isAccessible && companion.isAccessible override val callableName: String = method.name - override val valueParameters: List + override val valueParameters: List override val bucketGenerator: BucketGenerator init { // To prevent the call from failing, save the initial value and then rewrite the flag. if (!method.isAccessible) method.isAccessible = true - val kmFunction: KmFunction = companion.findFunctionByMethod(method)!! - val kmParameters = kmFunction.valueParameters + val function = companion.findFunctionByMethod(method)!! - valueParameters = kmParameters.map { ValueParameter(it) } + valueParameters = function.valueParameters val rawTypes = method.parameterTypes.asList() bucketGenerator = BucketGenerator( rawTypes, valueParameters, - kmParameters.mapToConverters(rawTypes, cache) + valueParameters.mapToConverters(rawTypes, cache) ) } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt index 095d1700..0862a2d8 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt @@ -7,8 +7,7 @@ import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.ArgumentBucket import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.BucketGenerator import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass -import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull -import kotlinx.metadata.KmValueParameter +import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter /** * A class that abstracts the creation of instances by calling KFunction. @@ -28,7 +27,7 @@ internal sealed class ValueCreator { /** * ValueParameters of the KFunction to be called. */ - abstract val valueParameters: List + abstract val valueParameters: List protected abstract val bucketGenerator: BucketGenerator @@ -61,12 +60,12 @@ internal sealed class ValueCreator { } @Suppress("UNCHECKED_CAST") -internal fun List.mapToConverters( +internal fun List.mapToConverters( rawTypes: List>, cache: ReflectionCache ): List?> = mapIndexed { i, param -> - param.type.reconstructClassOrNull() + param.reconstructedClassOrNull ?.takeIf { it.isUnboxableValueClass() && rawTypes[i] != it } ?.let { cache.getValueClassUnboxConverter(it) } } as List?> // Cast to cheat generics diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueParameter.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueParameter.kt deleted file mode 100644 index 27c9778b..00000000 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueParameter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator - -import kotlinx.metadata.KmClassifier -import kotlinx.metadata.KmType -import kotlinx.metadata.KmValueParameter -import kotlinx.metadata.declaresDefaultValue -import kotlinx.metadata.isNullable - -internal class ValueParameter(param: KmValueParameter) { - val name: String = param.name - val type: KmType = param.type - val isOptional: Boolean = param.declaresDefaultValue - val isNullable: Boolean = type.isNullable - val isVararg: Boolean = param.varargElementType != null - val isGenericType: Boolean = type.classifier is KmClassifier.TypeParameter - - // TODO: Formatting into a form that is easy to understand as an error message with reference to KParameter - override fun toString() = "parameter name: $name parameter type: ${type.classifier}" -} 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/JmClass.kt similarity index 64% rename from src/main/kotlin/io/github/projectmapk/jackson/module/kogera/JmClass.kt rename to src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmClass.kt index 63921758..2d72ae9d 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/JmClass.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmClass.kt @@ -1,13 +1,13 @@ -package io.github.projectmapk.jackson.module.kogera +package io.github.projectmapk.jackson.module.kogera.jmClass +import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull +import io.github.projectmapk.jackson.module.kogera.toDescBuilder +import io.github.projectmapk.jackson.module.kogera.toKmClass +import io.github.projectmapk.jackson.module.kogera.toSignature import kotlinx.metadata.ClassKind import kotlinx.metadata.ClassName import kotlinx.metadata.KmClass -import kotlinx.metadata.KmConstructor -import kotlinx.metadata.KmFunction -import kotlinx.metadata.KmProperty -import kotlinx.metadata.KmType -import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.isNullable import kotlinx.metadata.jvm.signature import kotlinx.metadata.kind import java.lang.reflect.Constructor @@ -20,30 +20,45 @@ internal sealed interface JmClass { private val companionField: Field = declaringClass.getDeclaredField(companionObject) val type: Class<*> = companionField.type val isAccessible: Boolean = companionField.isAccessible - private val functions by lazy { type.toKmClass()!!.functions } + private val factoryFunctions by lazy { + // Since it is a user-defined factory function that is processed, + // it always has arguments and the return value is the same as declaringClass. + type.toKmClass()!!.functions.mapNotNull { func -> + func + .takeIf { + func.valueParameters.isNotEmpty() && func.returnType.reconstructClassOrNull() == declaringClass + } + ?.let { JmFunction(it) } + } + } 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 companionField.get(null) } - fun findFunctionByMethod(method: Method): KmFunction? { + fun findFunctionByMethod(method: Method): JmFunction? { val signature = method.toSignature() - return functions.find { it.signature == signature } + return factoryFunctions.find { it.signature == signature } } } + // region: from KmClass val kind: ClassKind - val constructors: List + val constructors: List val sealedSubclasses: List - val inlineClassUnderlyingType: KmType? val propertyNameSet: Set - val properties: List + val properties: List val companion: CompanionObject? + // endregion - fun findKmConstructor(constructor: Constructor<*>): KmConstructor? - fun findPropertyByField(field: Field): KmProperty? - fun findPropertyByGetter(getter: Method): KmProperty? + // region: computed props + val wrapsNullableIfValue: Boolean + // endregion + + fun findJmConstructor(constructor: Constructor<*>): JmConstructor? + fun findPropertyByField(field: Field): JmProperty? + fun findPropertyByGetter(getter: Method): JmProperty? } private class JmClassImpl( @@ -52,16 +67,17 @@ private class JmClassImpl( superJmClass: JmClass?, interfaceJmClasses: List ) : JmClass { - private val allPropsMap: Map + private val allPropsMap: Map // Defined as non-lazy because it is always read in both serialization and deserialization - override val properties: List + override val properties: List private val companionPropName: String? = kmClass.companionObject override val kind: ClassKind = kmClass.kind - override val constructors: List = kmClass.constructors + override val constructors: List = kmClass.constructors.map { JmConstructor(it) } override val sealedSubclasses: List = kmClass.sealedSubclasses - override val inlineClassUnderlyingType: KmType? = kmClass.inlineClassUnderlyingType + + override val wrapsNullableIfValue: Boolean = kmClass.inlineClassUnderlyingType?.isNullable ?: false init { // Add properties of inherited classes and interfaces @@ -69,7 +85,7 @@ private class JmClassImpl( // it is necessary to obtain a more specific type, so always add it from the abstract class first. val tempPropsMap = ((superJmClass as JmClassImpl?)?.allPropsMap?.toMutableMap() ?: mutableMapOf()).apply { kmClass.properties.forEach { - this[it.name] = it + this[it.name] = JmProperty(it) } } @@ -90,7 +106,7 @@ private class JmClassImpl( companionPropName?.let { JmClass.CompanionObject(clazz, it) } } - override fun findKmConstructor(constructor: Constructor<*>): KmConstructor? { + override fun findJmConstructor(constructor: Constructor<*>): JmConstructor? { val descHead = constructor.parameterTypes.toDescBuilder() val len = descHead.length val desc = CharArray(len + 1).apply { @@ -111,11 +127,11 @@ private class JmClassImpl( } // Field name always matches property name - override fun findPropertyByField(field: Field): KmProperty? = allPropsMap[field.name] + override fun findPropertyByField(field: Field): JmProperty? = allPropsMap[field.name] - override fun findPropertyByGetter(getter: Method): KmProperty? { + override fun findPropertyByGetter(getter: Method): JmProperty? { val getterName = getter.name - return properties.find { it.getterSignature?.name == getterName } + return properties.find { it.getterName == getterName } } } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmConstructor.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmConstructor.kt new file mode 100644 index 00000000..67340239 --- /dev/null +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmConstructor.kt @@ -0,0 +1,18 @@ +package io.github.projectmapk.jackson.module.kogera.jmClass + +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.isSecondary +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.signature + +internal data class JmConstructor( + val isSecondary: Boolean, + val signature: JvmMethodSignature?, + val valueParameters: List +) { + constructor(constructor: KmConstructor) : this( + isSecondary = constructor.isSecondary, + signature = constructor.signature, + valueParameters = constructor.valueParameters.map { JmValueParameter(it) } + ) +} diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmFunction.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmFunction.kt new file mode 100644 index 00000000..27ef59ba --- /dev/null +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmFunction.kt @@ -0,0 +1,12 @@ +package io.github.projectmapk.jackson.module.kogera.jmClass + +import kotlinx.metadata.KmFunction +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.signature + +internal class JmFunction( + val signature: JvmMethodSignature?, + val valueParameters: List +) { + constructor(function: KmFunction) : this(function.signature, function.valueParameters.map { JmValueParameter(it) }) +} diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmProperty.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmProperty.kt new file mode 100644 index 00000000..c8201051 --- /dev/null +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmProperty.kt @@ -0,0 +1,21 @@ +package io.github.projectmapk.jackson.module.kogera.jmClass + +import kotlinx.metadata.KmProperty +import kotlinx.metadata.KmType +import kotlinx.metadata.jvm.JvmMemberSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.setterSignature + +internal data class JmProperty( + val name: String?, + val getterName: String?, + val setterSignature: JvmMemberSignature?, + val returnType: KmType +) { + constructor(kmProperty: KmProperty) : this( + name = kmProperty.name, + getterName = kmProperty.getterSignature?.name, + setterSignature = kmProperty.setterSignature, + returnType = kmProperty.returnType + ) +} diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmValueParameter.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmValueParameter.kt new file mode 100644 index 00000000..245c117b --- /dev/null +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/jmClass/JmValueParameter.kt @@ -0,0 +1,34 @@ +package io.github.projectmapk.jackson.module.kogera.jmClass + +import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeProjection +import kotlinx.metadata.KmValueParameter +import kotlinx.metadata.declaresDefaultValue +import kotlinx.metadata.isNullable + +internal class JmValueParameter( + val name: String, + val isOptional: Boolean, + val isVararg: Boolean, + type: KmType, + classifier: KmClassifier.Class? +) { + constructor(valueParameter: KmValueParameter) : this( + valueParameter.name, + isOptional = valueParameter.declaresDefaultValue, + isVararg = valueParameter.varargElementType != null, + valueParameter.type, + valueParameter.type.classifier as? KmClassifier.Class + ) + + val isNullable: Boolean = type.isNullable + val isGenericType: Boolean = type.classifier is KmClassifier.TypeParameter + val arguments: List = type.arguments + + val isString: Boolean = classifier?.name == "kotlin/String" + val reconstructedClassOrNull: Class<*>? by lazy { + classifier?.reconstructClassOrNull() + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt index fcf8e0f2..415f47e7 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt @@ -1,7 +1,7 @@ package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter -import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ValueParameter +import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals @@ -15,7 +15,7 @@ private class ArgumentBucketTest { every { this@mockk[any()] } returns null } - fun mockValueParameter(mockVararg: Boolean = false, mockOptional: Boolean = false) = mockk { + fun mockValueParameter(mockVararg: Boolean = false, mockOptional: Boolean = false) = mockk { every { isVararg } returns mockVararg every { isOptional } returns mockOptional }