From f0b886f129f618ac626d5ff54ec278dc0ea50e04 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Sun, 26 Dec 2021 17:07:52 -0600 Subject: [PATCH] Fix exhaustive when in JsonUtf8Reader (#1473) * Fix exhaustive when in JsonUtf8Reader This accidentally snuck into master because it predates the Kotlin 1.6 upgrade * Fix nextInt too * Clean up a few little warnings opportunistically * Fix comment * Use knownNotNull --- moshi/build.gradle.kts | 6 +- .../java/com/squareup/moshi/JsonUtf8Reader.kt | 84 +++++++++++++------ 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/moshi/build.gradle.kts b/moshi/build.gradle.kts index f82d45d..0383673 100644 --- a/moshi/build.gradle.kts +++ b/moshi/build.gradle.kts @@ -52,10 +52,12 @@ tasks.withType().configureEach { tasks.withType() .configureEach { kotlinOptions { + val args = mutableListOf("-Xopt-in=kotlin.contracts.ExperimentalContracts") if (name.contains("test", true)) { - @Suppress("SuspiciousCollectionReassignment") // It's not suspicious - freeCompilerArgs += listOf("-Xopt-in=kotlin.ExperimentalStdlibApi") + args += "-Xopt-in=kotlin.ExperimentalStdlibApi" } + @Suppress("SuspiciousCollectionReassignment") // It's not suspicious + freeCompilerArgs += args } } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.kt b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.kt index 7a75f29..cbaaa87 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.kt @@ -23,6 +23,7 @@ import okio.buffer import java.io.EOFException import java.io.IOException import java.math.BigDecimal +import kotlin.contracts.contract internal class JsonUtf8Reader : JsonReader { /** The input JSON. */ @@ -237,13 +238,13 @@ internal class JsonUtf8Reader : JsonReader { // "fallthrough" from previous `when` when (nextNonWhitespace(true).toChar()) { ']' -> { - if (peekStack == JsonScope.EMPTY_ARRAY) { - buffer.readByte() // Consume ']'. - return setPeeked(PEEKED_END_ARRAY) - } - // In lenient mode, a 0-length literal in an array means 'null'. return when (peekStack) { - JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY -> { + JsonScope.EMPTY_ARRAY -> { + 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) } @@ -320,11 +321,11 @@ internal class JsonUtf8Reader : JsonReader { // Confirm that chars [1..length) match the keyword. val length = keyword.length for (i in 1 until length) { - val i_long = i.toLong() - if (!source.request(i_long + 1)) { + val iAsLong = i.toLong() + if (!source.request(iAsLong + 1)) { return PEEKED_NONE } - c = buffer[i_long].asChar() + c = buffer[iAsLong].asChar() if (c != keyword[i] && c != keywordUpper[i]) { return PEEKED_NONE } @@ -622,19 +623,22 @@ internal class JsonUtf8Reader : JsonReader { pathIndices[stackSize - 1]++ return peekedLong.toDouble() } - val next = when { - p == PEEKED_NUMBER -> buffer.readUtf8(peekedNumberLength.toLong()) - p == PEEKED_DOUBLE_QUOTED -> nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) - p == PEEKED_SINGLE_QUOTED -> nextQuotedValue(SINGLE_QUOTE_OR_SLASH) - p == PEEKED_UNQUOTED -> nextUnquotedValue() - p != PEEKED_BUFFERED -> throw JsonDataException("Expected a double but was " + peek() + " at path " + path) + 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) } - peekedString = next peeked = PEEKED_BUFFERED val result = try { next.toDouble() } catch (e: NumberFormatException) { - throw JsonDataException("Expected a double but was $peekedString at path $path") + throw JsonDataException("Expected a double but was $next at path $path") } if (!lenient && (result.isNaN() || result.isInfinite())) { throw JsonEncodingException("JSON forbids NaN and infinities: $result at path $path") @@ -755,31 +759,42 @@ internal class JsonUtf8Reader : JsonReader { pathIndices[stackSize - 1]++ return result } - when (p) { - PEEKED_NUMBER -> peekedString = buffer.readUtf8(peekedNumberLength.toLong()) + val next: String = when (p) { + 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) else nextQuotedValue(SINGLE_QUOTE_OR_SLASH) + val next = if (p == PEEKED_DOUBLE_QUOTED) { + nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) + } else { + nextQuotedValue(SINGLE_QUOTE_OR_SLASH) + } + peekedString = next try { val result = next.toInt() - peekedString = next peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } catch (ignored: NumberFormatException) { // Fall back to parse as a double below. + next } } - else -> if (p != PEEKED_BUFFERED) throw JsonDataException("Expected an int but was ${peek()} at path $path") + 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 val asDouble = try { - peekedString!!.toDouble() + next.toDouble() } catch (e: NumberFormatException) { - throw JsonDataException("Expected an int but was $peekedString at path $path") + throw JsonDataException("Expected an int but was $next at path $path") } val result = asDouble.toInt() if (result.toDouble() != asDouble) { // Make sure no precision was lost casting to 'int'. - throw JsonDataException("Expected an int but was $peekedString at path $path") + throw JsonDataException("Expected an int but was $next at path $path") } peekedString = null peeked = PEEKED_NONE @@ -1026,11 +1041,13 @@ internal class JsonUtf8Reader : JsonReader { } } + @Suppress("NOTHING_TO_INLINE") private inline fun peekIfNone(): Int { val p = peeked return if (p == PEEKED_NONE) doPeek() else p } + @Suppress("NOTHING_TO_INLINE") private inline fun setPeeked(peekedType: Int): Int { peeked = peekedType return peekedType @@ -1079,4 +1096,21 @@ internal class JsonUtf8Reader : JsonReader { } } +@Suppress("NOTHING_TO_INLINE") private inline fun Byte.asChar(): Char = toInt().toChar() + +// Sneaky backdoor way of marking a value as non-null to the compiler and skip the null-check intrinsic. +// Safe to use (unstable) contracts since they're gone in the final bytecode +// TODO move this to Util.kt after it's migrated to kotlin +@Suppress("NOTHING_TO_INLINE") +private inline fun markNotNull(value: T?) { + contract { + returns() implies (value != null) + } +} + +@Suppress("NOTHING_TO_INLINE") +private inline fun knownNotNull(value: T?): T { + markNotNull(value) + return value +}