diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1862745..2b498c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,4 +37,4 @@ kotlinCompileTesting = { module = "dev.zacsweers.kctfork:core", version.ref = "k kotlinCompileTesting-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref ="kotlinCompileTesting" } truth = "com.google.truth:truth:1.4.1" googleJavaFormat = "com.google.googlejavaformat:google-java-format:1.20.0" -ktlint = "com.pinterest.ktlint:ktlint-cli:1.1.1" +ktlint = "com.pinterest.ktlint:ktlint-cli:1.2.0" diff --git a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt index 6ea3494..2fbadba 100644 --- a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt +++ b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt @@ -200,7 +200,9 @@ public class AdapterGenerator( private fun TypeSpec.createProguardRule(): ProguardConfig { val adapterConstructorParams = when (requireNotNull(primaryConstructor).parameters.size) { 1 -> listOf(CN_MOSHI.reflectionName()) + 2 -> listOf(CN_MOSHI.reflectionName(), "${CN_TYPE.reflectionName()}[]") + // Should never happen else -> error("Unexpected number of arguments on primary constructor: $primaryConstructor") } @@ -726,16 +728,26 @@ public data class PreparedAdapter(val spec: FileSpec, val proguardConfig: Progua private fun AsmType.toReflectionString(): String { return when (this) { AsmType.VOID_TYPE -> "void" + AsmType.BOOLEAN_TYPE -> "boolean" + AsmType.CHAR_TYPE -> "char" + AsmType.BYTE_TYPE -> "byte" + AsmType.SHORT_TYPE -> "short" + AsmType.INT_TYPE -> "int" + AsmType.FLOAT_TYPE -> "float" + AsmType.LONG_TYPE -> "long" + AsmType.DOUBLE_TYPE -> "double" + else -> when (sort) { AsmType.ARRAY -> "${elementType.toReflectionString()}[]" + // Object type else -> className } diff --git a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt index c7ad46b..06c7bd5 100644 --- a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt +++ b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt @@ -64,6 +64,7 @@ public data class DelegateKey( val (initializerString, args) = when { jsonQualifiers.isEmpty() -> ", %M()" to arrayOf(MemberName("kotlin.collections", "emptySet")) + else -> { ", setOf(%L)" to arrayOf(jsonQualifiers.map { it.asInstantiationExpression() }.joinToCode()) } diff --git a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeRenderer.kt b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeRenderer.kt index a0b040b..358e1e5 100644 --- a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeRenderer.kt +++ b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeRenderer.kt @@ -92,10 +92,12 @@ internal abstract class TypeRenderer { target = typeName.inTypes[0] method = "supertypeOf" } + typeName.outTypes.size == 1 -> { target = typeName.outTypes[0] method = "subtypeOf" } + else -> throw IllegalArgumentException( "Unrepresentable wildcard type. Cannot have more than one bound: $typeName", ) diff --git a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/kotlintypes.kt b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/kotlintypes.kt index 9e84a95..a43d52d 100644 --- a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/kotlintypes.kt +++ b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/kotlintypes.kt @@ -49,7 +49,9 @@ internal fun TypeName.rawType(): ClassName { internal fun TypeName.findRawType(): ClassName? { return when (this) { is ClassName -> this + is ParameterizedTypeName -> rawType + is LambdaTypeName -> { var count = parameters.size if (receiver != null) { @@ -62,6 +64,7 @@ internal fun TypeName.findRawType(): ClassName? { } ClassName("kotlin.jvm.functions", functionSimpleName) } + else -> null } } @@ -104,11 +107,14 @@ internal fun TypeName.asTypeBlock(): CodeBlock { rawType.asTypeBlock() } } + is TypeVariableName -> { val bound = bounds.firstOrNull() ?: ANY return bound.asTypeBlock() } + is LambdaTypeName -> return rawType().asTypeBlock() + is ClassName -> { // Check against the non-nullable version for equality, but we'll keep the nullability in // consideration when creating the CodeBlock if needed. @@ -121,10 +127,13 @@ internal fun TypeName.asTypeBlock(): CodeBlock { CodeBlock.of("%T::class.javaPrimitiveType!!", this) } } + UNIT, Void::class.asTypeName(), NOTHING -> throw IllegalStateException("Parameter with void, Unit, or Nothing type is illegal") + else -> CodeBlock.of("%T::class.java", copy(nullable = false)) } } + else -> throw UnsupportedOperationException("Parameter with type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, or type variables are allowed.") } } @@ -138,11 +147,15 @@ internal fun KModifier.checkIsVisibility() { internal fun TypeName.stripTypeVarVariance(resolver: TypeVariableResolver): TypeName { return when (this) { is ClassName -> this + is ParameterizedTypeName -> { deepCopy { it.stripTypeVarVariance(resolver) } } + is TypeVariableName -> resolver[name] + is WildcardTypeName -> deepCopy { it.stripTypeVarVariance(resolver) } + else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.") } } @@ -168,14 +181,17 @@ internal fun WildcardTypeName.deepCopy(transform: (TypeName) -> TypeName): TypeN // Consumer type - single element inTypes, single ANY element outType. return when { this == STAR -> this + outTypes.isNotEmpty() && inTypes.isEmpty() -> { WildcardTypeName.producerOf(transform(outTypes[0])) .copy(nullable = isNullable, annotations = annotations) } + inTypes.isNotEmpty() -> { WildcardTypeName.consumerOf(transform(inTypes[0])) .copy(nullable = isNullable, annotations = annotations) } + else -> throw UnsupportedOperationException("Not possible.") } } diff --git a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/typeAliasUnwrapping.kt b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/typeAliasUnwrapping.kt index 1e6b598..d8438a7 100644 --- a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/typeAliasUnwrapping.kt +++ b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/typeAliasUnwrapping.kt @@ -44,18 +44,23 @@ private fun TypeName.unwrapTypeAliasInternal(): TypeName? { internal fun TypeName.unwrapTypeAlias(): TypeName { return when (this) { is ClassName -> unwrapTypeAliasInternal() ?: this + is ParameterizedTypeName -> { unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias) } + is TypeVariableName -> { unwrapTypeAliasInternal() ?: deepCopy(transform = TypeName::unwrapTypeAlias) } + is WildcardTypeName -> { unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias) } + is LambdaTypeName -> { unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias) } + Dynamic -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.") } } diff --git a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt index cccc7bb..23a7a7f 100644 --- a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt +++ b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt @@ -152,6 +152,7 @@ internal fun targetType( ) return null } + !kmClass.isClass -> { messager.printMessage( ERROR, @@ -160,6 +161,7 @@ internal fun targetType( ) return null } + kmClass.isInner -> { messager.printMessage( ERROR, @@ -168,6 +170,7 @@ internal fun targetType( ) return null } + kmClass.flags.isSealed -> { messager.printMessage( ERROR, @@ -176,6 +179,7 @@ internal fun targetType( ) return null } + kmClass.flags.isAbstract -> { messager.printMessage( ERROR, @@ -184,6 +188,7 @@ internal fun targetType( ) return null } + kmClass.flags.isLocal -> { messager.printMessage( ERROR, @@ -192,6 +197,7 @@ internal fun targetType( ) return null } + !kmClass.flags.isPublic && !kmClass.flags.isInternal -> { messager.printMessage( ERROR, @@ -350,15 +356,18 @@ private fun resolveTypeArgs( return when { resolvedType !is TypeVariableName -> resolvedType + entryStartIndex != 0 -> { // We need to go deeper resolveTypeArgs(targetClass, resolvedType, resolvedTypes, allowedTypeVars, entryStartIndex - 1) } + resolvedType.copy(nullable = false) in allowedTypeVars -> { // This is a generic type in the top-level declared class. This is fine to leave in because // this will be handled by the `Type` array passed in at runtime. resolvedType } + else -> error("Could not find $resolvedType in $resolvedTypes. Also not present in allowable top-level type vars $allowedTypeVars") } } diff --git a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt index b2302e5..8e19ba9 100644 --- a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt +++ b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt @@ -76,6 +76,7 @@ private fun addValueToBlock(value: Any, resolver: Resolver, member: CodeBlock.Bu } member.add("⇤⇤)") } + is KSType -> { val unwrapped = value.unwrapTypeAlias() val isEnum = (unwrapped.declaration as KSClassDeclaration).classKind == ClassKind.ENUM_ENTRY @@ -87,13 +88,16 @@ private fun addValueToBlock(value: Any, resolver: Resolver, member: CodeBlock.Bu member.add("%T::class", unwrapped.toClassName()) } } + is KSName -> member.add( "%T.%L", ClassName.bestGuess(value.getQualifier()), value.getShortName(), ) + is KSAnnotation -> member.add("%L", value.toAnnotationSpec(resolver)) + else -> member.add(memberForValue(value)) } } @@ -105,13 +109,21 @@ private fun addValueToBlock(value: Any, resolver: Resolver, member: CodeBlock.Bu */ internal fun memberForValue(value: Any) = when (value) { is Class<*> -> CodeBlock.of("%T::class", value) + is Enum<*> -> CodeBlock.of("%T.%L", value.javaClass, value.name) + is String -> CodeBlock.of("%S", value) + is Float -> CodeBlock.of("%Lf", value) + is Double -> CodeBlock.of("%L", value) + is Char -> CodeBlock.of("$value.toChar()") + is Byte -> CodeBlock.of("$value.toByte()") + is Short -> CodeBlock.of("$value.toShort()") + // Int or Boolean else -> CodeBlock.of("%L", value) } diff --git a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/shadedUtil.kt b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/shadedUtil.kt index 553a541..de58602 100644 --- a/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/shadedUtil.kt +++ b/moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/shadedUtil.kt @@ -80,32 +80,39 @@ private fun KSAnnotation.createInvocationHandler(clazz: Class<*>): InvocationHan } when (val result = argument.value ?: method.defaultValue) { is Proxy -> result + is List<*> -> { val value = { result.asArray(method) } cache.getOrPut(Pair(method.returnType, result), value) } + else -> { when { method.returnType.isEnum -> { val value = { result.asEnum(method.returnType) } cache.getOrPut(Pair(method.returnType, result), value) } + method.returnType.isAnnotation -> { val value = { (result as KSAnnotation).asAnnotation(method.returnType) } cache.getOrPut(Pair(method.returnType, result), value) } + method.returnType.name == "java.lang.Class" -> { val value = { (result as KSType).asClass() } cache.getOrPut(Pair(method.returnType, result), value) } + method.returnType.name == "byte" -> { val value = { result.asByte() } cache.getOrPut(Pair(method.returnType, result), value) } + method.returnType.name == "short" -> { val value = { result.asShort() } cache.getOrPut(Pair(method.returnType, result), value) } + else -> result // original value } } @@ -129,27 +136,39 @@ private fun KSAnnotation.asAnnotation( private fun List<*>.asArray(method: Method) = when (method.returnType.componentType.name) { "boolean" -> (this as List).toBooleanArray() + "byte" -> (this as List).toByteArray() + "short" -> (this as List).toShortArray() + "char" -> (this as List).toCharArray() + "double" -> (this as List).toDoubleArray() + "float" -> (this as List).toFloatArray() + "int" -> (this as List).toIntArray() + "long" -> (this as List).toLongArray() + "java.lang.Class" -> (this as List).map { Class.forName(it.declaration.qualifiedName!!.asString()) }.toTypedArray() + "java.lang.String" -> (this as List).toTypedArray() + else -> { // arrays of enums or annotations when { method.returnType.componentType.isEnum -> { this.toArray(method) { result -> result.asEnum(method.returnType.componentType) } } + method.returnType.componentType.isAnnotation -> { this.toArray(method) { result -> (result as KSAnnotation).asAnnotation(method.returnType.componentType) } } + else -> throw IllegalStateException("Unable to process type ${method.returnType.componentType.name}") } } diff --git a/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt b/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt index b39b29d..b320f51 100644 --- a/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt +++ b/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt @@ -668,6 +668,7 @@ class JsonClassCodegenProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.Simple" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.Simple @@ -678,6 +679,7 @@ class JsonClassCodegenProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.Generic" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.Generic @@ -688,6 +690,7 @@ class JsonClassCodegenProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.UsingQualifiers" -> { assertThat(generatedFile.readText()).contains( """ @@ -700,6 +703,7 @@ class JsonClassCodegenProcessorTest { """.trimIndent(), ) } + "moshi-testPackage.MixedTypes" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.MixedTypes @@ -710,6 +714,7 @@ class JsonClassCodegenProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.DefaultParams" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.DefaultParams @@ -726,6 +731,7 @@ class JsonClassCodegenProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.Complex" -> { assertThat(generatedFile.readText()).contains( """ @@ -744,6 +750,7 @@ class JsonClassCodegenProcessorTest { """.trimIndent(), ) } + "moshi-testPackage.MultipleMasks" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.MultipleMasks @@ -760,6 +767,7 @@ class JsonClassCodegenProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.NestedType.NestedSimple" -> { assertThat(generatedFile.readText()).contains( """ @@ -772,6 +780,7 @@ class JsonClassCodegenProcessorTest { """.trimIndent(), ) } + else -> error("Unexpected proguard file! ${generatedFile.name}") } } diff --git a/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt b/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt index 37966a8..203e782 100644 --- a/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt +++ b/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt @@ -718,6 +718,7 @@ class JsonClassSymbolProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.Simple" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.Simple @@ -728,6 +729,7 @@ class JsonClassSymbolProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.Generic" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.Generic @@ -738,6 +740,7 @@ class JsonClassSymbolProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.UsingQualifiers" -> { assertThat(generatedFile.readText()).contains( """ @@ -750,6 +753,7 @@ class JsonClassSymbolProcessorTest { """.trimIndent(), ) } + "moshi-testPackage.MixedTypes" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.MixedTypes @@ -760,6 +764,7 @@ class JsonClassSymbolProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.DefaultParams" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.DefaultParams @@ -776,6 +781,7 @@ class JsonClassSymbolProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.Complex" -> { assertThat(generatedFile.readText()).contains( """ @@ -794,6 +800,7 @@ class JsonClassSymbolProcessorTest { """.trimIndent(), ) } + "moshi-testPackage.MultipleMasks" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.MultipleMasks @@ -810,6 +817,7 @@ class JsonClassSymbolProcessorTest { } """.trimIndent(), ) + "moshi-testPackage.NestedType.NestedSimple" -> { assertThat(generatedFile.readText()).contains( """ @@ -822,6 +830,7 @@ class JsonClassSymbolProcessorTest { """.trimIndent(), ) } + else -> error("Unexpected proguard file! ${generatedFile.name}") } } diff --git a/moshi-kotlin-tests/build.gradle.kts b/moshi-kotlin-tests/build.gradle.kts index be35331..c16cb09 100644 --- a/moshi-kotlin-tests/build.gradle.kts +++ b/moshi-kotlin-tests/build.gradle.kts @@ -24,9 +24,11 @@ when (testMode) { REFLECT -> { // Do nothing! } + KAPT -> { apply(plugin = "org.jetbrains.kotlin.kapt") } + KSP -> { apply(plugin = "com.google.devtools.ksp") } @@ -51,9 +53,11 @@ dependencies { REFLECT -> { // Do nothing } + KAPT -> { "kaptTest"(project(":moshi-kotlin-codegen")) } + KSP -> { "kspTest"(project(":moshi-kotlin-codegen")) } diff --git a/moshi-kotlin-tests/codegen-only/build.gradle.kts b/moshi-kotlin-tests/codegen-only/build.gradle.kts index 3066717..5fc56c0 100644 --- a/moshi-kotlin-tests/codegen-only/build.gradle.kts +++ b/moshi-kotlin-tests/codegen-only/build.gradle.kts @@ -25,9 +25,11 @@ when (testMode) { // Default to KSP. This is a CI-only thing apply(plugin = "com.google.devtools.ksp") } + KAPT -> { apply(plugin = "org.jetbrains.kotlin.kapt") } + KSP -> { apply(plugin = "com.google.devtools.ksp") } @@ -53,9 +55,11 @@ dependencies { // Default to KSP in this case, this is a CI-only thing "kspTest"(project(":moshi-kotlin-codegen")) } + KAPT -> { "kaptTest"(project(":moshi-kotlin-codegen")) } + KSP -> { "kspTest"(project(":moshi-kotlin-codegen")) } diff --git a/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterFactory.kt b/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterFactory.kt index 993751c..9024995 100644 --- a/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterFactory.kt +++ b/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterFactory.kt @@ -104,7 +104,10 @@ internal class KotlinJsonAdapter( if (values[i] === ABSENT_VALUE) { when { constructor.parameters[i].isOptional -> isFullInitialized = false - constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null. + + // Replace absent with null. + constructor.parameters[i].type.isMarkedNullable -> values[i] = null + else -> throw missingProperty( constructor.parameters[i].name, allBindings[i]?.jsonName, @@ -285,9 +288,11 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory { property.returnType.javaType } } + is KTypeParameter -> { property.returnType.javaType } + else -> error("Not possible!") } val resolvedPropertyType = propertyType.resolve(type, rawType) diff --git a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.kt b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.kt index 3eabb61..98c0e7f 100644 --- a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.kt +++ b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.kt @@ -61,7 +61,9 @@ internal class AdapterMethodsFactory( override fun toJson(writer: JsonWriter, value: Any?) { when { toAdapter == null -> knownNotNull(delegate).toJson(writer, value) + !toAdapter.nullable && value == null -> writer.nullValue() + else -> { try { toAdapter.toJson(moshi, writer, value) @@ -77,7 +79,9 @@ internal class AdapterMethodsFactory( override fun fromJson(reader: JsonReader): Any? { return when { fromAdapter == null -> knownNotNull(delegate).fromJson(reader) + !fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL -> reader.nextNull() + else -> { try { fromAdapter.fromJson(moshi, reader) @@ -161,6 +165,7 @@ internal class AdapterMethodsFactory( } } } + parameterTypes.size == 1 && returnType != Void.TYPE -> { // List pointToJson(Point point) { val returnTypeAnnotations = method.jsonAnnotations @@ -194,6 +199,7 @@ internal class AdapterMethodsFactory( } } } + else -> { throw IllegalArgumentException( """ @@ -249,6 +255,7 @@ internal class AdapterMethodsFactory( override fun fromJson(moshi: Moshi, reader: JsonReader) = invokeMethod(reader) } } + parameterTypes.size == 1 && returnType != Void.TYPE -> { // Point pointFromJson(List o) { val qualifierAnnotations = parameterAnnotations[0].jsonAnnotations @@ -279,6 +286,7 @@ internal class AdapterMethodsFactory( } } } + else -> { throw IllegalArgumentException( """ diff --git a/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.kt b/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.kt index 77a03db..24e3297 100644 --- a/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.kt +++ b/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.kt @@ -49,41 +49,49 @@ internal class ArrayJsonAdapter( elementAdapter.toJson(writer, element) } } + is ByteArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } + is CharArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } + is DoubleArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } + is FloatArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } + is IntArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } + is LongArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } + is ShortArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } + is Array<*> -> { for (element in value) { elementAdapter.toJson(writer, element) diff --git a/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.kt b/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.kt index da7d1f4..68f59d2 100644 --- a/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.kt +++ b/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.kt @@ -53,9 +53,11 @@ internal abstract class CollectionJsonAdapter, T> priv List::class.java, Collection::class.java -> { newArrayListAdapter(type, moshi).nullSafe() } + Set::class.java -> { newLinkedHashSetAdapter(type, moshi).nullSafe() } + else -> null } } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonReader.kt b/moshi/src/main/java/com/squareup/moshi/JsonReader.kt index 8b6440b..3354916 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonReader.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonReader.kt @@ -483,6 +483,7 @@ public sealed class JsonReader : Closeable { endArray() } } + Token.BEGIN_OBJECT -> { return buildMap { beginObject() @@ -497,10 +498,15 @@ public sealed class JsonReader : Closeable { endObject() } } + Token.STRING -> nextString() + Token.NUMBER -> nextDouble() + Token.BOOLEAN -> nextBoolean() + Token.NULL -> nextNull() + else -> throw IllegalStateException("Expected a value but was ${peek()} at path $path") } } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonScope.kt b/moshi/src/main/java/com/squareup/moshi/JsonScope.kt index 5845413..37f669e 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonScope.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonScope.kt @@ -59,12 +59,14 @@ internal object JsonScope { for (i in 0 until stackSize) { when (stack[i]) { EMPTY_ARRAY, NONEMPTY_ARRAY -> append('[').append(pathIndices[i]).append(']') + EMPTY_OBJECT, DANGLING_NAME, NONEMPTY_OBJECT -> { append('.') if (pathNames[i] != null) { append(pathNames[i]) } } + NONEMPTY_DOCUMENT, EMPTY_DOCUMENT, CLOSED -> {} } } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.kt b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.kt index 21a7981..6edc159 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.kt @@ -147,6 +147,7 @@ internal class JsonUtf8Reader : JsonReader { val peekStack = scopes[stackSize - 1] when (peekStack) { JsonScope.EMPTY_ARRAY -> scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY + JsonScope.NONEMPTY_ARRAY -> { // Look for a comma before the next element. val c = nextNonWhitespace(true).toChar() @@ -155,11 +156,16 @@ internal class JsonUtf8Reader : JsonReader { ']' -> { return setPeeked(PEEKED_END_ARRAY) } + ';' -> checkLenient() - ',' -> Unit /*no op*/ + + /*no op*/ + ',' -> Unit + else -> throw syntaxError("Unterminated array") } } + JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT -> { scopes[stackSize - 1] = JsonScope.DANGLING_NAME // Look for a comma before the next element. @@ -170,8 +176,12 @@ internal class JsonUtf8Reader : JsonReader { '}' -> { return setPeeked(PEEKED_END_OBJECT) } - ',' -> Unit /*no op*/ + + /*no op*/ + ',' -> Unit + ';' -> checkLenient() + else -> throw syntaxError("Unterminated object") } } @@ -180,17 +190,20 @@ internal class JsonUtf8Reader : JsonReader { buffer.readByte() // consume the '\"'. PEEKED_DOUBLE_QUOTED_NAME } + '\'' -> { buffer.readByte() // consume the '\''. checkLenient() PEEKED_SINGLE_QUOTED_NAME } + '}' -> if (peekStack != JsonScope.NONEMPTY_OBJECT) { buffer.readByte() // consume the '}'. PEEKED_END_OBJECT } else { throw syntaxError("Expected name") } + else -> { checkLenient() if (isLiteral(c.code)) { @@ -203,23 +216,29 @@ internal class JsonUtf8Reader : JsonReader { peeked = next return next } + JsonScope.DANGLING_NAME -> { scopes[stackSize - 1] = JsonScope.NONEMPTY_OBJECT // Look for a colon before the value. val c = nextNonWhitespace(true).toChar() buffer.readByte() // Consume ':'. when (c) { - ':' -> Unit /*no op*/ + /*no op*/ + ':' -> Unit + '=' -> { checkLenient() if (source.request(1) && buffer[0].asChar() == '>') { buffer.readByte() // Consume '>'. } } + else -> throw syntaxError("Expected ':'") } } + JsonScope.EMPTY_DOCUMENT -> scopes[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT + JsonScope.NONEMPTY_DOCUMENT -> { if (nextNonWhitespace(false) == -1) { return setPeeked(PEEKED_EOF) @@ -227,12 +246,14 @@ internal class JsonUtf8Reader : JsonReader { checkLenient() } } + JsonScope.STREAMING_VALUE -> { valueSource!!.discard() valueSource = null stackSize-- return doPeek() } + else -> check(peekStack != JsonScope.CLOSED) { "JsonReader is closed" } } // "fallthrough" from previous `when` @@ -243,39 +264,48 @@ internal class JsonUtf8Reader : JsonReader { buffer.readByte() // Consume ']'. setPeeked(PEEKED_END_ARRAY) } + JsonScope.NONEMPTY_ARRAY -> { // In lenient mode, a 0-length literal in an array means 'null'. checkLenient() setPeeked(PEEKED_NULL) } + else -> throw syntaxError("Unexpected value") } } + // In lenient mode, a 0-length literal in an array means 'null'. ';', ',' -> return when (peekStack) { JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY -> { checkLenient() setPeeked(PEEKED_NULL) } + else -> throw syntaxError("Unexpected value") } + '\'' -> { checkLenient() buffer.readByte() // Consume '\''. return setPeeked(PEEKED_SINGLE_QUOTED) } + '"' -> { buffer.readByte() // Consume '\"'. return setPeeked(PEEKED_DOUBLE_QUOTED) } + '[' -> { buffer.readByte() // Consume '['. return setPeeked(PEEKED_BEGIN_ARRAY) } + '{' -> { buffer.readByte() // Consume '{'. return setPeeked(PEEKED_BEGIN_OBJECT) } + else -> Unit /* no-op */ } var result = peekKeyword() @@ -305,16 +335,19 @@ internal class JsonUtf8Reader : JsonReader { keywordUpper = "TRUE" peeking = PEEKED_TRUE } + 'f', 'F' -> { keyword = "false" keywordUpper = "FALSE" peeking = PEEKED_FALSE } + 'n', 'N' -> { keyword = "null" keywordUpper = "NULL" peeking = PEEKED_NULL } + else -> return PEEKED_NONE } @@ -358,6 +391,7 @@ internal class JsonUtf8Reader : JsonReader { i++ continue } + NUMBER_CHAR_EXP_E -> { last = NUMBER_CHAR_EXP_SIGN i++ @@ -366,6 +400,7 @@ internal class JsonUtf8Reader : JsonReader { } return PEEKED_NONE } + '+' -> { if (last == NUMBER_CHAR_EXP_E) { last = NUMBER_CHAR_EXP_SIGN @@ -374,6 +409,7 @@ internal class JsonUtf8Reader : JsonReader { } return PEEKED_NONE } + 'e', 'E' -> { if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) { last = NUMBER_CHAR_EXP_E @@ -382,6 +418,7 @@ internal class JsonUtf8Reader : JsonReader { } return PEEKED_NONE } + '.' -> { if (last == NUMBER_CHAR_DIGIT) { last = NUMBER_CHAR_DECIMAL @@ -390,6 +427,7 @@ internal class JsonUtf8Reader : JsonReader { } return PEEKED_NONE } + else -> { if (c !in '0'..'9') { if (!isLiteral(c.code)) break @@ -400,6 +438,7 @@ internal class JsonUtf8Reader : JsonReader { value = -(c - '0').toLong() last = NUMBER_CHAR_DIGIT } + NUMBER_CHAR_DIGIT -> { if (value == 0L) { return PEEKED_NONE // Leading '0' prefix is not allowed (since it could be octal). @@ -413,7 +452,9 @@ internal class JsonUtf8Reader : JsonReader { ) value = newValue } + NUMBER_CHAR_DECIMAL -> last = NUMBER_CHAR_FRACTION_DIGIT + NUMBER_CHAR_EXP_E, NUMBER_CHAR_EXP_SIGN -> last = NUMBER_CHAR_EXP_DIGIT } } @@ -431,12 +472,14 @@ internal class JsonUtf8Reader : JsonReader { buffer.skip(i) setPeeked(PEEKED_LONG) } + last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT || last == NUMBER_CHAR_EXP_DIGIT -> { peekedNumberLength = i.toInt() setPeeked(PEEKED_NUMBER) } + else -> PEEKED_NONE } } @@ -448,8 +491,10 @@ internal class JsonUtf8Reader : JsonReader { checkLenient() // fall-through false } + // 0x000C = \f '{', '}', '[', ']', ':', ',', ' ', '\t', '\u000C', '\r', '\n' -> false + else -> true } } @@ -458,13 +503,17 @@ internal class JsonUtf8Reader : JsonReader { override fun nextName(): String { val result = when (peekIfNone()) { PEEKED_UNQUOTED_NAME -> nextUnquotedValue() + PEEKED_DOUBLE_QUOTED_NAME -> nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) + PEEKED_SINGLE_QUOTED_NAME -> nextQuotedValue(SINGLE_QUOTE_OR_SLASH) + PEEKED_BUFFERED_NAME -> { val name = peekedString!! peekedString = null name } + else -> throw JsonDataException("Expected a name but was ${peek()} at path $path") } peeked = PEEKED_NONE @@ -539,15 +588,21 @@ internal class JsonUtf8Reader : JsonReader { override fun nextString(): String { val result = when (peekIfNone()) { PEEKED_UNQUOTED -> nextUnquotedValue() + PEEKED_DOUBLE_QUOTED -> nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) + PEEKED_SINGLE_QUOTED -> nextQuotedValue(SINGLE_QUOTE_OR_SLASH) + PEEKED_BUFFERED -> { val buffered = peekedString!! peekedString = null buffered } + PEEKED_LONG -> peekedLong.toString() + PEEKED_NUMBER -> buffer.readUtf8(peekedNumberLength.toLong()) + else -> throw JsonDataException("Expected a string but was ${peek()} at path $path") } peeked = PEEKED_NONE @@ -601,11 +656,13 @@ internal class JsonUtf8Reader : JsonReader { pathIndices[stackSize - 1]++ true } + PEEKED_FALSE -> { peeked = PEEKED_NONE pathIndices[stackSize - 1]++ false } + else -> throw JsonDataException("Expected a boolean but was ${peek()} at path $path") } } @@ -630,13 +687,18 @@ internal class JsonUtf8Reader : JsonReader { } val next = when (p) { PEEKED_NUMBER -> buffer.readUtf8(peekedNumberLength.toLong()).also { peekedString = it } + PEEKED_DOUBLE_QUOTED -> nextQuotedValue(DOUBLE_QUOTE_OR_SLASH).also { peekedString = it } + PEEKED_SINGLE_QUOTED -> nextQuotedValue(SINGLE_QUOTE_OR_SLASH).also { peekedString = it } + PEEKED_UNQUOTED -> nextUnquotedValue().also { peekedString = it } + PEEKED_BUFFERED -> { // PEEKED_BUFFERED means the value's been stored in peekedString knownNotNull(peekedString) } + else -> throw JsonDataException("Expected a double but was " + peek() + " at path " + path) } peeked = PEEKED_BUFFERED @@ -663,6 +725,7 @@ internal class JsonUtf8Reader : JsonReader { } when { p == PEEKED_NUMBER -> peekedString = buffer.readUtf8(peekedNumberLength.toLong()) + p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED -> { peekedString = if (p == PEEKED_DOUBLE_QUOTED) nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) else nextQuotedValue(SINGLE_QUOTE_OR_SLASH) try { @@ -674,6 +737,7 @@ internal class JsonUtf8Reader : JsonReader { // Fall back to parse as a BigDecimal below. } } + p != PEEKED_BUFFERED -> { throw JsonDataException("Expected a long but was " + peek() + " at path " + path) } @@ -768,6 +832,7 @@ internal class JsonUtf8Reader : JsonReader { PEEKED_NUMBER -> { buffer.readUtf8(peekedNumberLength.toLong()).also { peekedString = it } } + PEEKED_DOUBLE_QUOTED, PEEKED_SINGLE_QUOTED -> { val next = if (p == PEEKED_DOUBLE_QUOTED) { nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) @@ -785,10 +850,12 @@ internal class JsonUtf8Reader : JsonReader { next } } + PEEKED_BUFFERED -> { // PEEKED_BUFFERED means the value's been stored in peekedString knownNotNull(peekedString) } + else -> throw JsonDataException("Expected an int but was ${peek()} at path $path") } peeked = PEEKED_BUFFERED @@ -826,10 +893,12 @@ internal class JsonUtf8Reader : JsonReader { pushScope(JsonScope.EMPTY_ARRAY) count++ } + PEEKED_BEGIN_OBJECT -> { pushScope(JsonScope.EMPTY_OBJECT) count++ } + PEEKED_END_ARRAY -> { count-- if (count < 0) { @@ -837,6 +906,7 @@ internal class JsonUtf8Reader : JsonReader { } stackSize-- } + PEEKED_END_OBJECT -> { count-- if (count < 0) { @@ -844,10 +914,15 @@ internal class JsonUtf8Reader : JsonReader { } stackSize-- } + PEEKED_UNQUOTED_NAME, PEEKED_UNQUOTED -> skipUnquotedValue() + PEEKED_DOUBLE_QUOTED, PEEKED_DOUBLE_QUOTED_NAME -> skipQuotedValue(DOUBLE_QUOTE_OR_SLASH) + PEEKED_SINGLE_QUOTED, PEEKED_SINGLE_QUOTED_NAME -> skipQuotedValue(SINGLE_QUOTE_OR_SLASH) + PEEKED_NUMBER -> buffer.skip(peekedNumberLength.toLong()) + PEEKED_EOF -> throw JsonDataException("Expected a value but was ${peek()} at path $path") } peeked = PEEKED_NONE @@ -867,29 +942,38 @@ internal class JsonUtf8Reader : JsonReader { state = JsonValueSource.STATE_JSON valueSourceStackSize++ } + PEEKED_BEGIN_OBJECT -> { prefix.writeUtf8("{") state = JsonValueSource.STATE_JSON valueSourceStackSize++ } + PEEKED_DOUBLE_QUOTED -> { prefix.writeUtf8("\"") state = JsonValueSource.STATE_DOUBLE_QUOTED } + PEEKED_SINGLE_QUOTED -> { prefix.writeUtf8("'") state = JsonValueSource.STATE_SINGLE_QUOTED } + PEEKED_NUMBER, PEEKED_LONG, PEEKED_UNQUOTED -> prefix.writeUtf8(nextString()) + PEEKED_TRUE -> prefix.writeUtf8("true") + PEEKED_FALSE -> prefix.writeUtf8("false") + PEEKED_NULL -> prefix.writeUtf8("null") + PEEKED_BUFFERED -> { val string = nextString() JsonWriter.of(prefix).use { jsonWriter -> jsonWriter.value(string) } } + else -> throw JsonDataException("Expected a value but was ${peek()} at path $path") } @@ -942,6 +1026,7 @@ internal class JsonUtf8Reader : JsonReader { p = 0 continue } + '/' -> { // skip a // end-of-line comment buffer.readByte() // '/' @@ -950,9 +1035,11 @@ internal class JsonUtf8Reader : JsonReader { p = 0 continue } + else -> c.code } } + '#' -> { // Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but it's // required to parse existing documents. @@ -960,6 +1047,7 @@ internal class JsonUtf8Reader : JsonReader { skipToEndOfLine() p = 0 } + else -> return c.code } } @@ -1026,12 +1114,20 @@ internal class JsonUtf8Reader : JsonReader { buffer.skip(4) result } + 't' -> '\t' + 'b' -> '\b' + 'n' -> '\n' + 'r' -> '\r' - 'f' -> '\u000C' /*\f*/ + + /*\f*/ + 'f' -> '\u000C' + '\n', '\'', '"', '\\', '/' -> escaped + else -> { if (!lenient) throw syntaxError("Invalid escape sequence: \\$escaped") escaped diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.kt b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.kt index 9955e39..4099b10 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.kt @@ -302,21 +302,27 @@ internal class JsonUtf8Writer( } nextTop = JsonScope.NONEMPTY_DOCUMENT } + JsonScope.EMPTY_DOCUMENT -> nextTop = JsonScope.NONEMPTY_DOCUMENT + JsonScope.NONEMPTY_ARRAY -> { sink.writeByte(','.code) newline() nextTop = JsonScope.NONEMPTY_ARRAY } + JsonScope.EMPTY_ARRAY -> { newline() nextTop = JsonScope.NONEMPTY_ARRAY } + JsonScope.DANGLING_NAME -> { nextTop = JsonScope.NONEMPTY_OBJECT sink.writeUtf8(separator) } + JsonScope.STREAMING_VALUE -> throw IllegalStateException("Sink from valueSink() was not closed") + else -> throw IllegalStateException("Nesting problem.") } replaceTop(nextTop) diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.kt b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.kt index 8a7e405..ff9e9e0 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.kt @@ -113,13 +113,21 @@ internal class JsonValueReader : JsonReader { // If the top of the stack is an iterator, take its first element and push it on the stack. return when (val peeked = stack[stackSize - 1]) { is JsonIterator -> peeked.endToken + is List<*> -> Token.BEGIN_ARRAY + is Map<*, *> -> Token.BEGIN_OBJECT + is Map.Entry<*, *> -> Token.NAME + is String -> Token.STRING + is Boolean -> Token.BOOLEAN + is Number -> Token.NUMBER + null -> Token.NULL + else -> ifNotClosed(peeked) { throw typeMismatch(peeked, "a JSON value") } @@ -170,10 +178,12 @@ internal class JsonValueReader : JsonReader { remove() peeked } + is Number -> { remove() peeked.toString() } + else -> ifNotClosed(peeked) { throw typeMismatch(peeked, Token.STRING) } @@ -211,6 +221,7 @@ internal class JsonValueReader : JsonReader { override fun nextDouble(): Double { val result = when (val peeked = require(Token.NUMBER)) { is Number -> peeked.toDouble() + is String -> { try { peeked.toDouble() @@ -218,6 +229,7 @@ internal class JsonValueReader : JsonReader { throw typeMismatch(peeked, Token.NUMBER) } } + else -> { throw typeMismatch(peeked, Token.NUMBER) } @@ -232,6 +244,7 @@ internal class JsonValueReader : JsonReader { override fun nextLong(): Long { val result: Long = when (val peeked = require(Token.NUMBER)) { is Number -> peeked.toLong() + is String -> try { peeked.toLong() } catch (e: NumberFormatException) { @@ -241,6 +254,7 @@ internal class JsonValueReader : JsonReader { throw typeMismatch(peeked, Token.NUMBER) } } + else -> throw typeMismatch(peeked, Token.NUMBER) } remove() @@ -250,6 +264,7 @@ internal class JsonValueReader : JsonReader { override fun nextInt(): Int { val result = when (val peeked = require(Token.NUMBER)) { is Number -> peeked.toInt() + is String -> try { peeked.toInt() } catch (e: NumberFormatException) { @@ -259,6 +274,7 @@ internal class JsonValueReader : JsonReader { throw typeMismatch(peeked, Token.NUMBER) } } + else -> throw typeMismatch(peeked, Token.NUMBER) } remove() diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueSource.kt b/moshi/src/main/java/com/squareup/moshi/JsonValueSource.kt index 1ef57f8..bbbca99 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueSource.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueSource.kt @@ -91,19 +91,23 @@ internal class JsonValueSource @JvmOverloads constructor( stackSize++ limit = index + 1 } + ']', '}' -> { stackSize-- if (stackSize == 0) state = STATE_END_OF_JSON limit = index + 1 } + '\"' -> { state = STATE_DOUBLE_QUOTED limit = index + 1 } + '\'' -> { state = STATE_SINGLE_QUOTED limit = index + 1 } + '/' -> { source.require(index + 2) when (buffer[index + 1]) { @@ -111,20 +115,24 @@ internal class JsonValueSource @JvmOverloads constructor( state = STATE_END_OF_LINE_COMMENT limit = index + 2 } + '*'.code.toByte() -> { state = STATE_C_STYLE_COMMENT limit = index + 2 } + else -> { limit = index + 1 } } } + '#' -> { state = STATE_END_OF_LINE_COMMENT limit = index + 1 } } + state === STATE_SINGLE_QUOTED || state === STATE_DOUBLE_QUOTED -> { if (b == '\\'.code.toByte()) { source.require(index + 2) @@ -134,6 +142,7 @@ internal class JsonValueSource @JvmOverloads constructor( limit = index + 1 } } + state === STATE_C_STYLE_COMMENT -> { source.require(index + 2) if (buffer[index + 1] == '/'.code.toByte()) { @@ -143,10 +152,12 @@ internal class JsonValueSource @JvmOverloads constructor( limit = index + 1 } } + state === STATE_END_OF_LINE_COMMENT -> { limit = index + 1 state = STATE_JSON } + else -> { throw AssertionError() } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt index e06051a..2f16f66 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt @@ -168,10 +168,13 @@ internal class JsonValueWriter : JsonWriter() { override fun value(value: Number?): JsonWriter = apply { when (value) { null -> nullValue() + // If it's trivially converted to a long, do that. is Byte, is Short, is Int, is Long -> value(value.toLong()) + // If it's trivially converted to a double, do that. is Float, is Double -> value(value.toDouble()) + else -> { // Everything else gets converted to a BigDecimal. val bigDecimalValue = if (value is BigDecimal) value else BigDecimal(value.toString()) @@ -229,6 +232,7 @@ internal class JsonValueWriter : JsonWriter() { scopes[stackSize - 1] = NONEMPTY_DOCUMENT stack[stackSize - 1] = newTop } + scope == EMPTY_OBJECT && deferredName != null -> { if (newTop != null || serializeNulls) { // Our maps always have string keys and object values. @@ -242,13 +246,16 @@ internal class JsonValueWriter : JsonWriter() { } deferredName = null } + scope == EMPTY_ARRAY -> { // Our lists always have object values. @Suppress("UNCHECKED_CAST") val list = stack[stackSize - 1] as MutableList list.add(newTop) } + scope == STREAMING_VALUE -> throw IllegalStateException("Sink from valueSink() was not closed") + else -> throw IllegalStateException("Nesting problem.") } return this diff --git a/moshi/src/main/java/com/squareup/moshi/JsonWriter.kt b/moshi/src/main/java/com/squareup/moshi/JsonWriter.kt index e866ccc..4d3bd51 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonWriter.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonWriter.kt @@ -151,22 +151,15 @@ public sealed class JsonWriter : Closeable, Flushable { * pretty printing. */ @JvmField - @Suppress("ktlint:standard:property-naming") // Exposed to sealed subtypes. + @Suppress("ktlint:standard:backing-property-naming") // Exposed to sealed subtypes. protected var _indent: String? = null - public open var indent: String - /** - * Returns a string containing only whitespace, used for each level of indentation. If empty, - * the encoded document will be compact. - */ - get() = _indent.orEmpty() - /** - * Sets the indentation string to be repeated for each level of indentation in the encoded - * document. If `indent.isEmpty()` the encoded document will be compact. Otherwise, the - * encoded document will be more human-readable. - * - * @param value a string containing only whitespace. - */ + /** + * A string containing only whitespace, used for each level of indentation. + * If empty, the encoded document will be compact. + */ + public open var indent: String + get() = _indent.orEmpty() set(value) { _indent = value.ifEmpty { null } } @@ -402,6 +395,7 @@ public sealed class JsonWriter : Closeable, Flushable { } endObject() } + is List<*> -> { beginArray() for (element in value) { @@ -409,12 +403,19 @@ public sealed class JsonWriter : Closeable, Flushable { } endArray() } + is String -> value(value as String?) + is Boolean -> value(value) + is Double -> value(value) + is Long -> value(value) + is Number -> value(value) + null -> nullValue() + else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}") } return this diff --git a/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.kt b/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.kt index 256fef7..9a68316 100644 --- a/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.kt +++ b/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.kt @@ -14,14 +14,10 @@ private val NATURAL_ORDER = Comparator { o1, o2 -> (o1 as Comparable). * removal. * * This implementation was derived from Android 4.1's TreeMap and LinkedHashMap classes. - */ -internal class LinkedHashTreeMap -/** - * Create a tree map ordered by [comparator]. This map's keys may only be null if [comparator] permits. * * @param comparator the comparator to order elements with, or null to use the natural ordering. */ -constructor( +internal class LinkedHashTreeMap( comparator: Comparator? = null, ) : AbstractMutableMap(), Serializable { @Suppress("UNCHECKED_CAST") @@ -402,6 +398,7 @@ constructor( break // no further rotations will be necessary } } + 2 -> { val leftLeft = left!!.left val leftRight = left.right @@ -417,12 +414,14 @@ constructor( break // no further rotations will be necessary } } + 0 -> { node.height = leftHeight + 1 // leftHeight == rightHeight if (insert) { break // the insert caused balance, so rebalancing is done! } } + else -> { assert(delta == -1 || delta == 1) node.height = max(leftHeight, rightHeight) + 1 @@ -748,6 +747,7 @@ internal class AvlBuilder { left.parent = center right.parent = center } + 1 -> { // Pop right and center, then make center the top of the stack. val right = stack @@ -759,6 +759,7 @@ internal class AvlBuilder { right.parent = center leavesSkipped = 0 } + 2 -> { leavesSkipped = 0 } diff --git a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.kt b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.kt index 83fff1e..11385b1 100644 --- a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.kt +++ b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.kt @@ -238,11 +238,17 @@ internal object StandardJsonAdapters : JsonAdapter.Factory { override fun fromJson(reader: JsonReader): Any? { return when (reader.peek()) { JsonReader.Token.BEGIN_ARRAY -> listJsonAdapter.fromJson(reader) + JsonReader.Token.BEGIN_OBJECT -> mapAdapter.fromJson(reader) + JsonReader.Token.STRING -> stringAdapter.fromJson(reader) + JsonReader.Token.NUMBER -> doubleAdapter.fromJson(reader) + JsonReader.Token.BOOLEAN -> booleanAdapter.fromJson(reader) + JsonReader.Token.NULL -> reader.nextNull() + else -> throw IllegalStateException( "Expected a value but was ${reader.peek()} at path ${reader.path}", ) diff --git a/moshi/src/main/java/com/squareup/moshi/Types.kt b/moshi/src/main/java/com/squareup/moshi/Types.kt index 387de8f..43eab2d 100644 --- a/moshi/src/main/java/com/squareup/moshi/Types.kt +++ b/moshi/src/main/java/com/squareup/moshi/Types.kt @@ -163,22 +163,27 @@ public object Types { // type is a normal class. type } + is ParameterizedType -> { // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but // suspects some pathological case related to nested classes exists. val rawType = type.rawType rawType as Class<*> } + is GenericArrayType -> { val componentType = type.genericComponentType java.lang.reflect.Array.newInstance(getRawType(componentType), 0).javaClass } + is TypeVariable<*> -> { // We could use the variable's bounds, but that won't work if there are multiple. having a raw // type that's more general than necessary is okay. Any::class.java } + is WildcardType -> getRawType(type.upperBounds[0]) + else -> { val className = type?.javaClass?.name?.toString() throw IllegalArgumentException("Expected a Class, ParameterizedType, or GenericArrayType, but <$type> is of type $className") @@ -222,6 +227,7 @@ public object Types { a == b // Class already specifies equals(). } } + is ParameterizedType -> { // Class instance with generic info, from method return types if (b is Class<*> && a.rawType == b.rawType) { @@ -235,6 +241,7 @@ public object Types { (a.rawType == b.rawType) && aTypeArguments.contentEquals(bTypeArguments) ) } + is GenericArrayType -> { if (b is Class<*>) { return equals(b.componentType, a.genericComponentType) @@ -242,14 +249,17 @@ public object Types { if (b !is GenericArrayType) return false return equals(a.genericComponentType, b.genericComponentType) } + is WildcardType -> { if (b !is WildcardType) return false return (a.upperBounds.contentEquals(b.upperBounds) && a.lowerBounds.contentEquals(b.lowerBounds)) } + is TypeVariable<*> -> { if (b !is TypeVariable<*>) return false return (a.genericDeclaration === b.genericDeclaration && (a.name == b.name)) } + else -> return false // This isn't a supported type. } } @@ -305,12 +315,16 @@ public object Types { ) { proxy, method, args -> when (method.name) { "annotationType" -> annotationType + "equals" -> { val o = args[0] annotationType.isInstance(o) } + "hashCode" -> 0 + "toString" -> "@${annotationType.name}()" + else -> method.invoke(proxy, *args) } } as T diff --git a/moshi/src/main/java/com/squareup/moshi/internal/Util.kt b/moshi/src/main/java/com/squareup/moshi/internal/Util.kt index 5c29a86..fdd1959 100644 --- a/moshi/src/main/java/com/squareup/moshi/internal/Util.kt +++ b/moshi/src/main/java/com/squareup/moshi/internal/Util.kt @@ -176,18 +176,22 @@ internal fun Type.canonicalize(): Type { is Class<*> -> { if (isArray) GenericArrayTypeImpl(this@canonicalize.componentType.canonicalize()) else this } + is ParameterizedType -> { if (this is ParameterizedTypeImpl) return this ParameterizedTypeImpl(ownerType, rawType, *actualTypeArguments) } + is GenericArrayType -> { if (this is GenericArrayTypeImpl) return this GenericArrayTypeImpl(genericComponentType) } + is WildcardType -> { if (this is WildcardTypeImpl) return this WildcardTypeImpl(upperBounds, lowerBounds) } + else -> this // This type is unsupported! } } @@ -226,18 +230,21 @@ private fun Type.resolve( toResolve = resolveTypeVariable(context, contextRawType, typeVariable) if (toResolve === typeVariable) return toResolve } + toResolve is Class<*> && toResolve.isArray -> { val original = toResolve val componentType: Type = original.componentType val newComponentType = componentType.resolve(context, contextRawType, visitedTypeVariables) return if (componentType === newComponentType) original else newComponentType.asArrayType() } + toResolve is GenericArrayType -> { val original = toResolve val componentType = original.genericComponentType val newComponentType = componentType.resolve(context, contextRawType, visitedTypeVariables) return if (componentType === newComponentType) original else newComponentType.asArrayType() } + toResolve is ParameterizedType -> { val original = toResolve val ownerType: Type? = original.ownerType @@ -258,6 +265,7 @@ private fun Type.resolve( } return if (changed) ParameterizedTypeImpl(newOwnerType, original.rawType, *args) else original } + toResolve is WildcardType -> { val original = toResolve val originalLowerBound = original.lowerBounds @@ -275,6 +283,7 @@ private fun Type.resolve( } return original } + else -> return toResolve } }