From b72dfe2f19fddf49fe09acaa6bc22bb91d3240bc Mon Sep 17 00:00:00 2001 From: Googler Date: Mon, 10 Mar 2025 05:40:49 -0700 Subject: [PATCH] [J2KT] Propagate inferred nullability in array literals and new array expressions, which were causing insertion of `!!` in value expressions. PiperOrigin-RevId: 735341118 --- .../j2cl/transpiler/backend/Backend.java | 2 + .../passes/PropagateNullability.java | 106 ++++++++++++++++++ .../integration/java/nullability/Main.java | 16 +-- .../output_kt/NotNullAssertionProblems.kt.txt | 31 ++--- ...labilityInferenceWithLocalVariables.kt.txt | 2 +- .../StreamCollectWildcardProblem.kt.txt | 9 +- 6 files changed, 131 insertions(+), 35 deletions(-) create mode 100644 transpiler/java/com/google/j2cl/transpiler/passes/PropagateNullability.java diff --git a/transpiler/java/com/google/j2cl/transpiler/backend/Backend.java b/transpiler/java/com/google/j2cl/transpiler/backend/Backend.java index 2466108f99..66124c18bb 100644 --- a/transpiler/java/com/google/j2cl/transpiler/backend/Backend.java +++ b/transpiler/java/com/google/j2cl/transpiler/backend/Backend.java @@ -166,6 +166,7 @@ import com.google.j2cl.transpiler.passes.PreventSmartCasts; import com.google.j2cl.transpiler.passes.PropagateCompileTimeConstants; import com.google.j2cl.transpiler.passes.PropagateConstants; +import com.google.j2cl.transpiler.passes.PropagateNullability; import com.google.j2cl.transpiler.passes.PropagateNullabilityInOverrides; import com.google.j2cl.transpiler.passes.RecoverShortcutBooleanOperator; import com.google.j2cl.transpiler.passes.RemoveCustomIsInstanceMethods; @@ -728,6 +729,7 @@ public ImmutableList> getPassFactories(BackendOption FixJavaKotlinMethodOverrideMismatch::new, AnnotateProtobufMethodsAsKtProperties::new, RewriteAnnotationTypesJ2kt::new, + PropagateNullability::new, NormalizeNullLiterals::new, NormalizeMinValueIntegralLiterals::new, CreateImplicitConstructors::new, diff --git a/transpiler/java/com/google/j2cl/transpiler/passes/PropagateNullability.java b/transpiler/java/com/google/j2cl/transpiler/passes/PropagateNullability.java new file mode 100644 index 0000000000..cec2e859eb --- /dev/null +++ b/transpiler/java/com/google/j2cl/transpiler/passes/PropagateNullability.java @@ -0,0 +1,106 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.j2cl.transpiler.passes; + +import com.google.j2cl.transpiler.ast.AbstractRewriter; +import com.google.j2cl.transpiler.ast.ArrayLiteral; +import com.google.j2cl.transpiler.ast.ArrayTypeDescriptor; +import com.google.j2cl.transpiler.ast.CompilationUnit; +import com.google.j2cl.transpiler.ast.Expression; +import com.google.j2cl.transpiler.ast.NewArray; +import com.google.j2cl.transpiler.ast.Node; +import com.google.j2cl.transpiler.ast.TypeDescriptor; +import java.util.stream.Stream; + +/** + * Propagates nullability in inferred types from actual nullability in expressions. + * + *

At this moment it propagates nullability of inferred component types in array literals, by + * inferring them from value expressions. Eventually, it'll also fix type arguments in invocations + * by inferring them from arguments. + * + *

In the following statement: + * + *

{@code
+ * @Nullable String[] arr = {null};
+ * }
+ * + * the inferred type of array literal is {@code String[]} instead of {@code @Nullable String[]}. It + * causes insertion of {@code !!} in transpiled Kotlin code: + * + *
{@code
+ * val arr: Array = arrayOf(null!!) as Array
+ * }
+ * + * This pass fixes the inferred array component type to be {@code @Nullable String[]}, which fixes + * the problem in transpiled Kotlin code: + * + *
{@code
+ * val arr: Array = arrayOf(null)
+ * }
+ */ +public class PropagateNullability extends AbstractJ2ktNormalizationPass { + + @Override + public void applyTo(CompilationUnit compilationUnit) { + compilationUnit.accept( + new AbstractRewriter() { + @Override + public Node rewriteArrayLiteral(ArrayLiteral arrayLiteral) { + return propagateNullabilityFromValueExpressions(arrayLiteral); + } + + @Override + public Node rewriteNewArray(NewArray newArray) { + Expression initializer = newArray.getInitializer(); + if (initializer == null) { + return newArray; + } + // Update type of NewArray expression from rewritten initializer. + return NewArray.Builder.from(newArray) + .setTypeDescriptor((ArrayTypeDescriptor) initializer.getTypeDescriptor()) + .build(); + } + }); + } + + private ArrayLiteral propagateNullabilityFromValueExpressions(ArrayLiteral arrayLiteral) { + ArrayTypeDescriptor arrayTypeDescriptor = arrayLiteral.getTypeDescriptor(); + TypeDescriptor componentTypeDescriptor = + propagateNullabilityFrom( + arrayTypeDescriptor.getComponentTypeDescriptor(), + arrayLiteral.getValueExpressions().stream().map(Expression::getTypeDescriptor)); + return arrayLiteral.toBuilder() + .setTypeDescriptor( + ArrayTypeDescriptor.Builder.from(arrayTypeDescriptor) + .setComponentTypeDescriptor(componentTypeDescriptor) + .build()) + .build(); + } + + private static TypeDescriptor propagateNullabilityFrom( + TypeDescriptor typeDescriptor, TypeDescriptor from) { + return !from.equals(typeDescriptor) && from.canBeNull() + ? typeDescriptor.toNullable() + : typeDescriptor; + } + + private static TypeDescriptor propagateNullabilityFrom( + TypeDescriptor typeDescriptor, Stream fromTypeDescriptors) { + return fromTypeDescriptors.reduce( + typeDescriptor, PropagateNullability::propagateNullabilityFrom, (a, b) -> a); + } +} diff --git a/transpiler/javatests/com/google/j2cl/integration/java/nullability/Main.java b/transpiler/javatests/com/google/j2cl/integration/java/nullability/Main.java index b98f423dc2..e3b3b19cda 100644 --- a/transpiler/javatests/com/google/j2cl/integration/java/nullability/Main.java +++ b/transpiler/javatests/com/google/j2cl/integration/java/nullability/Main.java @@ -89,23 +89,17 @@ private static final class Box { } private static void testArrayLiteral() { - // TODO(b/324550390): Remove the condition when the bug is fixed. - if (!isJ2Kt()) { - @Nullable String[] unusedArray1 = {STRING, NULL_STRING}; - @Nullable String[] unusedArray2 = {NULL_STRING, STRING}; - } + @Nullable String[] unusedArray1 = {STRING, NULL_STRING}; + @Nullable String[] unusedArray2 = {NULL_STRING, STRING}; } private static void testNewArray() { @Nullable String[] unusedArray1 = new @Nullable String[] {STRING, NULL_STRING}; @Nullable String[] unusedArray2 = new @Nullable String[] {NULL_STRING, STRING}; - // TODO(b/324550390): Remove the condition when the bug is fixed. - if (!isJ2Kt()) { - // Lack of @Nullable annotation in array creation expression should not cause NULL_STRING!! - @Nullable String[] unusedArray3 = new String[] {STRING, NULL_STRING}; - @Nullable String[] unusedArray4 = new String[] {NULL_STRING, STRING}; - } + // Lack of @Nullable annotation in array creation expression should not cause NULL_STRING!! + @Nullable String[] unusedArray3 = new String[] {STRING, NULL_STRING}; + @Nullable String[] unusedArray4 = new String[] {NULL_STRING, STRING}; } private static void testExplicitInvocationTypeArguments() { diff --git a/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/NotNullAssertionProblems.kt.txt b/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/NotNullAssertionProblems.kt.txt index 7341d4c8c5..1895643a55 100644 --- a/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/NotNullAssertionProblems.kt.txt +++ b/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/NotNullAssertionProblems.kt.txt @@ -37,11 +37,11 @@ open class NotNullAssertionProblems { @ObjCName("withNSString") string: String, @ObjCName("withNSString") nullableString: String? ) { - val array0: Array = arrayOf(null!!) as Array - val array1: Array = arrayOf(string, null!!) as Array - val array2: Array = arrayOf(string, nullableString!!) as Array - val array3: Array = arrayOf(null!!, string) as Array - val array4: Array = arrayOf(nullableString!!, string) as Array + val array0: Array = arrayOf(null) + val array1: Array = arrayOf(string, null) + val array2: Array = arrayOf(string, nullableString) + val array3: Array = arrayOf(null, string) + val array4: Array = arrayOf(nullableString, string) } @ObjCName("testNewArray") @@ -104,16 +104,13 @@ open class NotNullAssertionProblems { ) NotNullAssertionProblems.accept2(nullableString, string) NotNullAssertionProblems.acceptVararg( - string, - null!!, + *(arrayOf(string, null) as Array)!!, ) NotNullAssertionProblems.acceptVararg( - string, - nullableString!!, + *(arrayOf(string, nullableString) as Array)!!, ) NotNullAssertionProblems.acceptVararg( - null!!, - string, + *(arrayOf(null, string) as Array)!!, ) NotNullAssertionProblems.acceptVararg(nullableString, string) NotNullAssertionProblems.acceptGeneric( @@ -174,16 +171,13 @@ open class NotNullAssertionProblems { ) NotNullAssertionProblems.Consumer(nullableString, string) NotNullAssertionProblems.VarargConsumer( - string, - null!!, + *(arrayOf(string, null) as Array)!!, ) NotNullAssertionProblems.VarargConsumer( - string, - nullableString!!, + *(arrayOf(string, nullableString) as Array)!!, ) NotNullAssertionProblems.VarargConsumer( - null!!, - string, + *(arrayOf(null, string) as Array)!!, ) NotNullAssertionProblems.VarargConsumer(nullableString, string) NotNullAssertionProblems.GenericConsumer( @@ -292,8 +286,7 @@ open class NotNullAssertionProblems { null!!, ) NotNullAssertionProblems.VarargConsumer( - string, - null!!, + *(arrayOf(string, null) as Array)!!, ).accept( null!!, ) diff --git a/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/NullabilityInferenceWithLocalVariables.kt.txt b/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/NullabilityInferenceWithLocalVariables.kt.txt index ba4ab07925..465ac966da 100644 --- a/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/NullabilityInferenceWithLocalVariables.kt.txt +++ b/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/NullabilityInferenceWithLocalVariables.kt.txt @@ -38,7 +38,7 @@ open class NullabilityInferenceWithLocalVariables { @ObjCName("testArray") fun testArray(): Array { val local: String = "" - return arrayOf(local, "") + return arrayOf(local, "") as Array } @JvmStatic diff --git a/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/StreamCollectWildcardProblem.kt.txt b/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/StreamCollectWildcardProblem.kt.txt index 7cb9b5a737..0c7391a14c 100644 --- a/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/StreamCollectWildcardProblem.kt.txt +++ b/transpiler/javatests/com/google/j2cl/readable/java/j2kt/output_kt/StreamCollectWildcardProblem.kt.txt @@ -27,10 +27,12 @@ import java.util.stream.Collector import java.util.stream.Collectors import java.util.stream.Stream import kotlin.Any +import kotlin.Array import kotlin.Boolean import kotlin.OptIn import kotlin.String import kotlin.Suppress +import kotlin.arrayOf import kotlin.collections.MutableList import kotlin.experimental.ExperimentalObjCName import kotlin.jvm.JvmStatic @@ -144,16 +146,15 @@ open class StreamCollectWildcardProblem { @ObjCName("withBoolean") b: Boolean ): Stream { return Stream.of>( - (if (b) stream1.map( + *(arrayOf?>(if (b) stream1.map( Function/* , out String> */ { arg0: StreamCollectWildcardProblem.Foo -> return@Function arg0.getString() }, - ) else null)!!, - (if (b) stream2.map( + ) else null, if (b) stream2.map( Function/* , out String> */ { arg0_1: StreamCollectWildcardProblem.Foo -> return@Function arg0_1.getString() }, - ) else null)!!, + ) else null) as Array>)!!, ).filter( Predicate/* > */ { arg0_2: Stream -> return@Predicate Objects.nonNull(arg0_2)