[JVM IR] Maintain KT-36625 bug compatibility between non-IR and IR
backends by removing IMPLICIT_NOTNULL casts from IrStringConcatenation
arguments.
Also fixed an issue where IrStringConcatenation can be lowered into
a null String instead of a literal "null" String if the lone argument
was a platform type String or String with enhanced nullability and the
value was null (e.g., "${FromJava.nullPlatformString()}").
This commit is contained in:
committed by
Dmitry Petrov
parent
18a3d7ee08
commit
ba606147c9
+5
-1
@@ -7,6 +7,8 @@ package org.jetbrains.kotlin.backend.jvm.lower
|
|||||||
|
|
||||||
import org.jetbrains.kotlin.backend.common.FileLoweringPass
|
import org.jetbrains.kotlin.backend.common.FileLoweringPass
|
||||||
import org.jetbrains.kotlin.backend.common.lower.SpecialBridgeMethods
|
import org.jetbrains.kotlin.backend.common.lower.SpecialBridgeMethods
|
||||||
|
import org.jetbrains.kotlin.backend.common.lower.flattenStringConcatenationPhase
|
||||||
|
import org.jetbrains.kotlin.backend.common.lower.loops.forLoopsPhase
|
||||||
import org.jetbrains.kotlin.backend.common.phaser.makeIrFilePhase
|
import org.jetbrains.kotlin.backend.common.phaser.makeIrFilePhase
|
||||||
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
|
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
|
||||||
import org.jetbrains.kotlin.config.ApiVersion
|
import org.jetbrains.kotlin.config.ApiVersion
|
||||||
@@ -22,7 +24,9 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs
|
|||||||
val jvmArgumentNullabilityAssertions = makeIrFilePhase(
|
val jvmArgumentNullabilityAssertions = makeIrFilePhase(
|
||||||
::JvmArgumentNullabilityAssertionsLowering,
|
::JvmArgumentNullabilityAssertionsLowering,
|
||||||
name = "ArgumentNullabilityAssertions",
|
name = "ArgumentNullabilityAssertions",
|
||||||
description = "Transform nullability assertions on arguments according to the compiler settings"
|
description = "Transform nullability assertions on arguments according to the compiler settings",
|
||||||
|
// jvmStringConcatenationLowering may remove IMPLICIT_NOTNULL casts.
|
||||||
|
prerequisite = setOf(jvmStringConcatenationLowering)
|
||||||
)
|
)
|
||||||
|
|
||||||
private enum class AssertionScope {
|
private enum class AssertionScope {
|
||||||
|
|||||||
+22
-6
@@ -24,6 +24,8 @@ import org.jetbrains.kotlin.ir.declarations.IrFile
|
|||||||
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
|
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
|
||||||
import org.jetbrains.kotlin.ir.expressions.IrExpression
|
import org.jetbrains.kotlin.ir.expressions.IrExpression
|
||||||
import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation
|
import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation
|
||||||
|
import org.jetbrains.kotlin.ir.expressions.IrTypeOperator
|
||||||
|
import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall
|
||||||
import org.jetbrains.kotlin.ir.types.*
|
import org.jetbrains.kotlin.ir.types.*
|
||||||
import org.jetbrains.kotlin.ir.util.constructors
|
import org.jetbrains.kotlin.ir.util.constructors
|
||||||
import org.jetbrains.kotlin.ir.util.functions
|
import org.jetbrains.kotlin.ir.util.functions
|
||||||
@@ -86,9 +88,6 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun JvmIrBuilder.callToString(expression: IrExpression): IrExpression {
|
private fun JvmIrBuilder.callToString(expression: IrExpression): IrExpression {
|
||||||
if (expression.type.isString())
|
|
||||||
return expression
|
|
||||||
|
|
||||||
val argument = widenIntegerType(expression)
|
val argument = widenIntegerType(expression)
|
||||||
val argumentType = if (argument.type.isPrimitiveType()) argument.type else context.irBuiltIns.anyNType
|
val argumentType = if (argument.type.isPrimitiveType()) argument.type else context.irBuiltIns.anyNType
|
||||||
|
|
||||||
@@ -115,6 +114,18 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
|
|||||||
override fun visitStringConcatenation(expression: IrStringConcatenation): IrExpression {
|
override fun visitStringConcatenation(expression: IrStringConcatenation): IrExpression {
|
||||||
expression.transformChildrenVoid(this)
|
expression.transformChildrenVoid(this)
|
||||||
return context.createJvmIrBuilder(currentScope!!.scope.scopeOwnerSymbol, expression.startOffset, expression.endOffset).run {
|
return context.createJvmIrBuilder(currentScope!!.scope.scopeOwnerSymbol, expression.startOffset, expression.endOffset).run {
|
||||||
|
// When `String.plus(Any?)` is invoked with receiver of platform type String or String with enhanced nullability, this SHOULD
|
||||||
|
// fail a nullability check (NullPointerException) on the receiver. However, the non-IR backend has a bug (KT-36625) where this
|
||||||
|
// check is not inserted (see KT-36625). To maintain bug compatibility with the non-IR backend, we remove IMPLICIT_NOTNULL casts
|
||||||
|
// (which generate the nullability checks in JvmArgumentNullabilityAssertionsLowering) from all arguments.
|
||||||
|
|
||||||
|
fun IrExpression.unwrapImplicitNotNull() =
|
||||||
|
if (this is IrTypeOperatorCall && operator == IrTypeOperator.IMPLICIT_NOTNULL)
|
||||||
|
argument
|
||||||
|
else
|
||||||
|
this
|
||||||
|
|
||||||
|
|
||||||
val arguments = expression.arguments.map { lowerInlineClassArgument(it) }
|
val arguments = expression.arguments.map { lowerInlineClassArgument(it) }
|
||||||
|
|
||||||
when {
|
when {
|
||||||
@@ -122,11 +133,13 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
|
|||||||
irString("")
|
irString("")
|
||||||
|
|
||||||
arguments.size == 1 ->
|
arguments.size == 1 ->
|
||||||
callToString(arguments.single())
|
callToString(arguments.single().unwrapImplicitNotNull())
|
||||||
|
|
||||||
arguments.size == 2 && arguments[0].type.isStringClassType() ->
|
arguments.size == 2 && arguments[0].type.isStringClassType() ->
|
||||||
irCall(backendContext.ir.symbols.intrinsicStringPlus).apply {
|
irCall(backendContext.ir.symbols.intrinsicStringPlus).apply {
|
||||||
putValueArgument(0, arguments[0])
|
putValueArgument(0, arguments[0].unwrapImplicitNotNull())
|
||||||
|
|
||||||
|
// Unwrapping IMPLICIT_NOTNULL is not strictly necessary on 2nd argument (parameter type is `Any?`)
|
||||||
putValueArgument(1, arguments[1])
|
putValueArgument(1, arguments[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +150,10 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
|
|||||||
val appendFunction = typeToAppendFunction(argument.type)
|
val appendFunction = typeToAppendFunction(argument.type)
|
||||||
stringBuilder = irCall(appendFunction).apply {
|
stringBuilder = irCall(appendFunction).apply {
|
||||||
dispatchReceiver = stringBuilder
|
dispatchReceiver = stringBuilder
|
||||||
putValueArgument(0, argument)
|
|
||||||
|
// Unwrapping IMPLICIT_NOTNULL is necessary for ALL arguments. There could be a call to `String.plus(Any?)`
|
||||||
|
// anywhere in the flattened IrStringConcatenation expression, e.g., `"foo" + (Java.platformString() + 123)`.
|
||||||
|
putValueArgument(0, argument.unwrapImplicitNotNull())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
irCall(toStringFunction).apply {
|
irCall(toStringFunction).apply {
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// IGNORE_BACKEND_FIR: JVM_IR
|
||||||
|
// TARGET_BACKEND: JVM
|
||||||
|
// WITH_RUNTIME
|
||||||
|
|
||||||
|
// FILE: J.java
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
public class J {
|
||||||
|
public static String platformStringIsNull() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static String notNullStringIsNull() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String nullableStringIsNull() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static String notNullStringIsNotNull() {
|
||||||
|
return "foo";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String platformStringIsNotNull() {
|
||||||
|
return "foo";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String nullableStringIsNotNull() {
|
||||||
|
return "foo";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FILE: main.kt
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val n = 123
|
||||||
|
|
||||||
|
// Due to KT-36625, certain concatenation calls below (marked with a comment) SHOULD fail a nullability check but do not.
|
||||||
|
|
||||||
|
assertEquals("null", "${J.platformStringIsNull()}")
|
||||||
|
assertEquals("nullBAR", J.platformStringIsNull() + "BAR") // KT-36625
|
||||||
|
assertEquals("nullBAR", "${J.platformStringIsNull() + "BAR"}") // KT-36625
|
||||||
|
assertEquals("nullBAR", "${J.platformStringIsNull()}BAR")
|
||||||
|
assertEquals("BARnull", "BAR" + J.platformStringIsNull())
|
||||||
|
assertEquals("BARnull", "BAR${J.platformStringIsNull()}")
|
||||||
|
assertEquals("123null", "$n${J.platformStringIsNull()}")
|
||||||
|
assertEquals("nullBAR123", J.platformStringIsNull() + "BAR" + n) // KT-36625
|
||||||
|
assertEquals("nullBAR123", "${J.platformStringIsNull() + "BAR" + n}") // KT-36625
|
||||||
|
assertEquals("nullBAR123", "${J.platformStringIsNull()}BAR$n")
|
||||||
|
assertEquals("BARnull123", "BAR" + J.platformStringIsNull() + n)
|
||||||
|
assertEquals("BARnull123", "BAR${J.platformStringIsNull()}$n")
|
||||||
|
assertEquals("BARnull123", "BAR" + (J.platformStringIsNull() + n)) // KT-36625
|
||||||
|
assertEquals("123nullBAR", "$n${J.platformStringIsNull() + "BAR"}") // KT-36625
|
||||||
|
|
||||||
|
assertEquals("null", "${J.notNullStringIsNull()}")
|
||||||
|
assertEquals("nullBAR", J.notNullStringIsNull() + "BAR") // KT-36625
|
||||||
|
assertEquals("nullBAR", "${J.notNullStringIsNull() + "BAR"}") // KT-36625
|
||||||
|
assertEquals("nullBAR", "${J.notNullStringIsNull()}BAR")
|
||||||
|
assertEquals("BARnull", "BAR" + J.notNullStringIsNull())
|
||||||
|
assertEquals("BARnull", "BAR${J.notNullStringIsNull()}")
|
||||||
|
assertEquals("123null", "$n${J.notNullStringIsNull()}")
|
||||||
|
assertEquals("nullBAR123", J.notNullStringIsNull() + "BAR" + n) // KT-36625
|
||||||
|
assertEquals("nullBAR123", "${J.notNullStringIsNull() + "BAR" + n}") // KT-36625
|
||||||
|
assertEquals("nullBAR123", "${J.notNullStringIsNull()}BAR$n")
|
||||||
|
assertEquals("BARnull123", "BAR" + J.notNullStringIsNull() + n)
|
||||||
|
assertEquals("BARnull123", "BAR${J.notNullStringIsNull()}$n")
|
||||||
|
assertEquals("BARnull123", "BAR" + (J.notNullStringIsNull() + n)) // KT-36625
|
||||||
|
assertEquals("123nullBAR", "$n${J.notNullStringIsNull() + "BAR"}") // KT-36625
|
||||||
|
|
||||||
|
assertEquals("null", "${J.nullableStringIsNull()}")
|
||||||
|
assertEquals("nullBAR", J.nullableStringIsNull() + "BAR")
|
||||||
|
assertEquals("nullBAR", "${J.nullableStringIsNull() + "BAR"}")
|
||||||
|
assertEquals("nullBAR", "${J.nullableStringIsNull()}BAR")
|
||||||
|
assertEquals("BARnull", "BAR" + J.nullableStringIsNull())
|
||||||
|
assertEquals("BARnull", "BAR${J.nullableStringIsNull()}")
|
||||||
|
assertEquals("123null", "$n${J.nullableStringIsNull()}")
|
||||||
|
assertEquals("nullBAR123", J.nullableStringIsNull() + "BAR" + n)
|
||||||
|
assertEquals("nullBAR123", "${J.nullableStringIsNull() + "BAR" + n}")
|
||||||
|
assertEquals("nullBAR123", "${J.nullableStringIsNull()}BAR$n")
|
||||||
|
assertEquals("BARnull123", "BAR" + J.nullableStringIsNull() + n)
|
||||||
|
assertEquals("BARnull123", "BAR${J.nullableStringIsNull()}$n")
|
||||||
|
assertEquals("BARnull123", "BAR" + (J.nullableStringIsNull() + n))
|
||||||
|
assertEquals("123nullBAR", "$n${J.nullableStringIsNull() + "BAR"}")
|
||||||
|
|
||||||
|
assertEquals("foo", "${J.platformStringIsNotNull()}")
|
||||||
|
assertEquals("fooBAR", J.platformStringIsNotNull() + "BAR")
|
||||||
|
assertEquals("fooBAR", "${J.platformStringIsNotNull() + "BAR"}")
|
||||||
|
assertEquals("fooBAR", "${J.platformStringIsNotNull()}BAR")
|
||||||
|
assertEquals("BARfoo", "BAR" + J.platformStringIsNotNull())
|
||||||
|
assertEquals("BARfoo", "BAR${J.platformStringIsNotNull()}")
|
||||||
|
assertEquals("123foo", "$n${J.platformStringIsNotNull()}")
|
||||||
|
assertEquals("fooBAR123", J.platformStringIsNotNull() + "BAR" + n)
|
||||||
|
assertEquals("fooBAR123", "${J.platformStringIsNotNull() + "BAR" + n}")
|
||||||
|
assertEquals("fooBAR123", "${J.platformStringIsNotNull()}BAR$n")
|
||||||
|
assertEquals("BARfoo123", "BAR" + J.platformStringIsNotNull() + n)
|
||||||
|
assertEquals("BARfoo123", "BAR${J.platformStringIsNotNull()}$n")
|
||||||
|
assertEquals("BARfoo123", "BAR" + (J.platformStringIsNotNull() + n))
|
||||||
|
assertEquals("123fooBAR", "$n${J.platformStringIsNotNull() + "BAR"}")
|
||||||
|
|
||||||
|
assertEquals("foo", "${J.notNullStringIsNotNull()}")
|
||||||
|
assertEquals("fooBAR", J.notNullStringIsNotNull() + "BAR")
|
||||||
|
assertEquals("fooBAR", "${J.notNullStringIsNotNull() + "BAR"}")
|
||||||
|
assertEquals("fooBAR", "${J.notNullStringIsNotNull()}BAR")
|
||||||
|
assertEquals("BARfoo", "BAR" + J.notNullStringIsNotNull())
|
||||||
|
assertEquals("BARfoo", "BAR${J.notNullStringIsNotNull()}")
|
||||||
|
assertEquals("123foo", "$n${J.notNullStringIsNotNull()}")
|
||||||
|
assertEquals("fooBAR123", J.notNullStringIsNotNull() + "BAR" + n)
|
||||||
|
assertEquals("fooBAR123", "${J.notNullStringIsNotNull() + "BAR" + n}")
|
||||||
|
assertEquals("fooBAR123", "${J.notNullStringIsNotNull()}BAR$n")
|
||||||
|
assertEquals("BARfoo123", "BAR" + J.notNullStringIsNotNull() + n)
|
||||||
|
assertEquals("BARfoo123", "BAR${J.notNullStringIsNotNull()}$n")
|
||||||
|
assertEquals("BARfoo123", "BAR" + (J.notNullStringIsNotNull() + n))
|
||||||
|
assertEquals("123fooBAR", "$n${J.notNullStringIsNotNull() + "BAR"}")
|
||||||
|
|
||||||
|
assertEquals("foo", "${J.nullableStringIsNotNull()}")
|
||||||
|
assertEquals("fooBAR", J.nullableStringIsNotNull() + "BAR")
|
||||||
|
assertEquals("fooBAR", "${J.nullableStringIsNotNull() + "BAR"}")
|
||||||
|
assertEquals("fooBAR", "${J.nullableStringIsNotNull()}BAR")
|
||||||
|
assertEquals("BARfoo", "BAR" + J.nullableStringIsNotNull())
|
||||||
|
assertEquals("BARfoo", "BAR${J.nullableStringIsNotNull()}")
|
||||||
|
assertEquals("123foo", "$n${J.nullableStringIsNotNull()}")
|
||||||
|
assertEquals("fooBAR123", J.nullableStringIsNotNull() + "BAR" + n)
|
||||||
|
assertEquals("fooBAR123", "${J.nullableStringIsNotNull() + "BAR" + n}")
|
||||||
|
assertEquals("fooBAR123", "${J.nullableStringIsNotNull()}BAR$n")
|
||||||
|
assertEquals("BARfoo123", "BAR" + J.nullableStringIsNotNull() + n)
|
||||||
|
assertEquals("BARfoo123", "BAR${J.nullableStringIsNotNull()}$n")
|
||||||
|
assertEquals("BARfoo123", "BAR" + (J.nullableStringIsNotNull() + n))
|
||||||
|
assertEquals("123fooBAR", "$n${J.nullableStringIsNotNull() + "BAR"}")
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
// IGNORE_BACKEND: JVM
|
|
||||||
|
|
||||||
fun f(s: String) = "$s"
|
fun f(s: String) = "$s"
|
||||||
|
|
||||||
fun g(s: String?) = "$s"
|
fun g(s: String?) = "$s"
|
||||||
|
|
||||||
// 1 valueOf
|
// 2 valueOf
|
||||||
// 0 NEW java/lang/StringBuilder
|
// 0 NEW java/lang/StringBuilder
|
||||||
+5
@@ -27490,6 +27490,11 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
|
|||||||
runTest("compiler/testData/codegen/box/strings/stringBuilderAppend.kt");
|
runTest("compiler/testData/codegen/box/strings/stringBuilderAppend.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("stringFromJavaPlus.kt")
|
||||||
|
public void testStringFromJavaPlus() throws Exception {
|
||||||
|
runTest("compiler/testData/codegen/box/strings/stringFromJavaPlus.kt");
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("stringPlusOnlyWorksOnString.kt")
|
@TestMetadata("stringPlusOnlyWorksOnString.kt")
|
||||||
public void testStringPlusOnlyWorksOnString() throws Exception {
|
public void testStringPlusOnlyWorksOnString() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/strings/stringPlusOnlyWorksOnString.kt");
|
runTest("compiler/testData/codegen/box/strings/stringPlusOnlyWorksOnString.kt");
|
||||||
|
|||||||
+5
@@ -26307,6 +26307,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
|
|||||||
runTest("compiler/testData/codegen/box/strings/stringBuilderAppend.kt");
|
runTest("compiler/testData/codegen/box/strings/stringBuilderAppend.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("stringFromJavaPlus.kt")
|
||||||
|
public void testStringFromJavaPlus() throws Exception {
|
||||||
|
runTest("compiler/testData/codegen/box/strings/stringFromJavaPlus.kt");
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("stringPlusOnlyWorksOnString.kt")
|
@TestMetadata("stringPlusOnlyWorksOnString.kt")
|
||||||
public void testStringPlusOnlyWorksOnString() throws Exception {
|
public void testStringPlusOnlyWorksOnString() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/strings/stringPlusOnlyWorksOnString.kt");
|
runTest("compiler/testData/codegen/box/strings/stringPlusOnlyWorksOnString.kt");
|
||||||
|
|||||||
+5
@@ -25994,6 +25994,11 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT
|
|||||||
runTest("compiler/testData/codegen/box/strings/stringBuilderAppend.kt");
|
runTest("compiler/testData/codegen/box/strings/stringBuilderAppend.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("stringFromJavaPlus.kt")
|
||||||
|
public void testStringFromJavaPlus() throws Exception {
|
||||||
|
runTest("compiler/testData/codegen/box/strings/stringFromJavaPlus.kt");
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("stringPlusOnlyWorksOnString.kt")
|
@TestMetadata("stringPlusOnlyWorksOnString.kt")
|
||||||
public void testStringPlusOnlyWorksOnString() throws Exception {
|
public void testStringPlusOnlyWorksOnString() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/strings/stringPlusOnlyWorksOnString.kt");
|
runTest("compiler/testData/codegen/box/strings/stringPlusOnlyWorksOnString.kt");
|
||||||
|
|||||||
+5
@@ -25994,6 +25994,11 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
|
|||||||
runTest("compiler/testData/codegen/box/strings/stringBuilderAppend.kt");
|
runTest("compiler/testData/codegen/box/strings/stringBuilderAppend.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("stringFromJavaPlus.kt")
|
||||||
|
public void testStringFromJavaPlus() throws Exception {
|
||||||
|
runTest("compiler/testData/codegen/box/strings/stringFromJavaPlus.kt");
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("stringPlusOnlyWorksOnString.kt")
|
@TestMetadata("stringPlusOnlyWorksOnString.kt")
|
||||||
public void testStringPlusOnlyWorksOnString() throws Exception {
|
public void testStringPlusOnlyWorksOnString() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/strings/stringPlusOnlyWorksOnString.kt");
|
runTest("compiler/testData/codegen/box/strings/stringPlusOnlyWorksOnString.kt");
|
||||||
|
|||||||
Reference in New Issue
Block a user