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..96d950183 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 @@ -1544,7 +1544,21 @@ && sameUpToStates(c.parameterizedType(), target)) { // Optional.ofNullable(element) into an Optional: Kotlin's Optional bounds its T non-null, so the call // is an Optional, which the invariant Optional is not. Erased, the cast checks nothing KotlinContext.message(KotlinPrintCode.UNCHECKED_CAST, e, KotlinContext.describe(e)); - return new OutputBuilderImpl().add(SymbolEnum.LEFT_PARENTHESIS).add(print(e, q)).add(SpaceEnum.ONE) + OutputBuilder call = print(e, q); + String owner = KotlinTypeName.staticOwner(mc.methodInfo().typeInfo(), q); + if ("empty".equals(mc.methodInfo().name()) && mc.typeArguments().isEmpty()) { + // the cast gives Kotlin nothing to infer empty's T from: Optional's own + call = text(owner + ".empty<" + KotlinTypeName.of(target.parameters().getFirst(), q) + ">()"); + } else if ("of".equals(mc.methodInfo().name()) && mc.parameterExpressions().size() == 1 + && unwrap(mc.parameterExpressions().getFirst()).parameterizedType().typeParameter() != null) { + // Optional.of(value) with a value of type T: Kotlin's of takes a T & Any; Java's throws on null too + Expression value = mc.parameterExpressions().getFirst(); + KotlinContext.message(KotlinPrintCode.ASSERT_AT_DEREFERENCE, value, KotlinContext.describe(value)); + call = new OutputBuilderImpl().add(new TextImpl(owner)).add(SymbolEnum.DOT).add(new TextImpl("of")) + .add(SymbolEnum.LEFT_PARENTHESIS).add(receiver(value, q)).add(KotlinNullability.NOT_NULL) + .add(SymbolEnum.RIGHT_PARENTHESIS); + } + return new OutputBuilderImpl().add(SymbolEnum.LEFT_PARENTHESIS).add(call).add(SpaceEnum.ONE) .add(KotlinKeyword.AS).add(SpaceEnum.ONE) .add(new TextImpl(KotlinTypeName.of(target.withNullable(io.codelaser.maddi.cst.api.type.NullableState.NONNULL), q))) .add(SymbolEnum.RIGHT_PARENTHESIS); diff --git a/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinStructure.java b/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinStructure.java index aa0bac386..264c83c20 100644 --- a/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinStructure.java +++ b/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinStructure.java @@ -1992,6 +1992,28 @@ class Box { compiles(kotlin); } + /** + * Optional.empty() and Optional.of(value) into an Optional, beside ofNullable above: the cast to Optional + * gives Kotlin nothing to infer the call's own T from. + */ + @Test + public void optionalEmptyAndOfIntoTypeVariable() { + String kotlin = kotlin(""" + package a; + import java.util.Optional; + class Opt { + private final T value; + Opt(T value) { this.value = value; } + Optional none() { return Optional.empty(); } + Optional some() { return Optional.of(value); } + } + """); + // empty's T from Optional; of's value asserted, as Kotlin's of takes a T & Any (Java's of throws on null too) + contains(kotlin, "(Optional.empty() as Optional)"); + contains(kotlin, "(Optional.of(value!!) as Optional)"); + compiles(kotlin); + } + /** guava's CaseFormat: constants override a non-abstract method of their enum, which Kotlin wants open. */ @Test public void enumConstantOverridesOpenMethod() {