diff --git a/maddi-cst-print-kotlin/src/main/java/io/codelaser/maddi/cst/print/kotlin/KotlinExpressionPrinter.java b/maddi-cst-print-kotlin/src/main/java/io/codelaser/maddi/cst/print/kotlin/KotlinExpressionPrinter.java index 9e87794b5..3b7313e06 100644 --- a/maddi-cst-print-kotlin/src/main/java/io/codelaser/maddi/cst/print/kotlin/KotlinExpressionPrinter.java +++ b/maddi-cst-print-kotlin/src/main/java/io/codelaser/maddi/cst/print/kotlin/KotlinExpressionPrinter.java @@ -449,13 +449,43 @@ && nullableElements(object)) { .add(new TextImpl("ignoreCase = true")).add(SymbolEnum.RIGHT_PARENTHESIS); } // Iterators.<@Nullable Entry>getNext(it, null): the type-use annotation's state, as in a class header - String typeArguments = !mc.typeArguments().isEmpty() ? mc.typeArguments().stream() - .map(t -> KotlinTypeName.of(KotlinTypePrinter.declared(t), q)).collect(java.util.stream.Collectors.joining(", ", "<", ">")) + String typeArguments = !mc.typeArguments().isEmpty() ? explicitTypeArguments(mc, q) : comparingKeyCall(mc) ? comparedType(mc, q) : resultOnlyOrLambdaOnly(mc, q); b.add(new TextImpl(KotlinNames.name(mc.methodInfo().name()) + (typeArguments == null ? "" : typeArguments))); return b.add(arguments(mc.parameterExpressions(), mc.methodInfo(), mc, q)); } + /** + * Java's explicit type arguments, {@code Ranges.range(2, 4)}, with the type-use annotation's state; and, + * where {@link #widened} prints the call into a declaration whose type argument is nullable, with that state: + * Kotlin's generics are invariant, {@code range} is no {@code MutableList}. + */ + private static String explicitTypeArguments(MethodCall mc, Qualification q) { + List arguments = new ArrayList<>(mc.typeArguments().stream().map(KotlinTypePrinter::declared).toList()); + java.util.Map.Entry t = TYPE_ARGUMENTS_TARGET.get(); + io.codelaser.maddi.cst.api.info.MethodInfo m = mc.methodInfo(); + if (t != null && t.getKey() == mc && m.returnType() != null && m.typeParameters().size() == arguments.size()) { + Map concrete; + try { + concrete = m.returnType().formalToConcrete(t.getValue()); + } catch (RuntimeException | AssertionError e) { + concrete = Map.of(); + } + for (int i = 0; i < arguments.size(); i++) { + ParameterizedType target = concrete.get(m.typeParameters().get(i)); + ParameterizedType argument = arguments.get(i); + if (target != null && target.wildcard() == null && KotlinNullability.isNullable(target) + && sameUpToStates(target, argument)) { + arguments.set(i, argument.withNullable(io.codelaser.maddi.cst.api.type.NullableState.NULLABLE)); + } + } + } + return arguments.stream().map(a -> KotlinTypeName.of(a, q)).collect(java.util.stream.Collectors.joining(", ", "<", ">")); + } + + /** The call with explicit type arguments that {@link #widened} prints into a declaration of this type. */ + private static final ThreadLocal> TYPE_ARGUMENTS_TARGET = new ThreadLocal<>(); + /** The method returns a type whose type arguments Kotlin types nullable: {@code Ordering<@Nullable S>}. */ private static boolean nullableArgument(io.codelaser.maddi.cst.api.info.MethodInfo methodInfo) { ParameterizedType verdict = KotlinNullability.returnType(methodInfo); @@ -1561,6 +1591,19 @@ && sameUpToStates(c.parameterizedType(), target)) { TO_ARRAY_TARGET.set(previous); } } + if (target != null && target.arrays() == 0 && unwrap(e) instanceof MethodCall mc && mc.methodInfo() != null + && !mc.typeArguments().isEmpty() && target.parameters().stream().anyMatch(KotlinNullability::isNullable) + && !optionalOfNullable(e, target) && !rawOf(rawValueType(e), target)) { + // List r = Ranges.range(2, 4) into a MutableList: Kotlin's generics are invariant, + // so the explicit type arguments take the states of what the call initializes, range + java.util.Map.Entry previous = TYPE_ARGUMENTS_TARGET.get(); + TYPE_ARGUMENTS_TARGET.set(java.util.Map.entry(mc, target)); + try { + return print(e, q); + } finally { + TYPE_ARGUMENTS_TARGET.set(previous); + } + } if (unwrap(e) instanceof Cast c && target != null && target.arrays() > 0 && c.parameterizedType().arrays() == target.arrays() && sameUpToStates(c.parameterizedType(), target) && KotlinTypeName.primitiveArray(c.parameterizedType().componentType()) == null) { diff --git a/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinNullability.java b/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinNullability.java index d85d12cfe..adaec7d34 100644 --- a/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinNullability.java +++ b/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinNullability.java @@ -1961,4 +1961,29 @@ static int size() { contains(kotlin, "val r: MutableList"); compiles(kotlin); } + + /** + * As {@link #localTypedWhereGenericCallInfersNonNull}, with Java's explicit type argument: {@code + * Ranges2.range(2, 4)} into a local whose elements are nullable, because {@code r.add(null)} writes a null + * into them. Kotlin's generics are invariant: the explicit type argument takes the local's state, {@code + * range}; {@code range} is a {@code MutableList}, which the {@code MutableList} local rejects. + */ + @Test + public void explicitTypeArgumentTakesLocalTypeArgumentState() { + KotlinPrintOptions options = new KotlinPrintOptions(new ByName(Set.of("r<>")), KotlinPrintOptions.NullCheck.ASSERT); + String kotlin = kotlin(""" + package a; + import java.util.*; + class Ranges2 { + static List range(T lower, T upper) { List l = new ArrayList<>(); l.add(lower); l.add(upper); return l; } + static int size() { + List r = Ranges2.range(2, 4); + r.add(null); + return r.size(); + } + } + """, options); + contains(kotlin, "range(2, 4)"); + compiles(kotlin); + } }