From 93a1c3927ba24725cca32dc8725704bdaf5d4f5f Mon Sep 17 00:00:00 2001 From: Tom Tourwe Date: Sun, 18 Oct 2026 13:25:40 +0200 Subject: [PATCH] Kotlin printer: a method reference returned or assigned as a Java functional interface gets its SAM constructor `static Function parser() { return Util::parse; }` printed `= Util ::parse`, which Kotlin types as a KFunction1, no java.util.function.Function (return type mismatch); a local `Function f = Util::parse` lost its declared type the same way. As an argument Kotlin converts the reference itself, which is why that case compiled. Now: `Function(Util ::parse)` for returned and assigned values, when the interface has no wildcard type arguments; arguments are unchanged. Test: TestJavaToKotlinCalls.staticReferenceReturnedAsFunction (output compiles with kotlinc). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tom Tourwe --- .../print/kotlin/KotlinExpressionPrinter.java | 9 +++++++ .../print/kotlin/TestJavaToKotlinCalls.java | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) 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..a5e42e939 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 @@ -1462,6 +1462,15 @@ private static boolean spread(List args, io.codelaser.maddi.cst.api. * an array. */ static OutputBuilder assigned(Expression e, ParameterizedType target, Qualification q) { + if (KotlinContext.translatingJava() && unwrap(e) instanceof MethodReference && target != null + && target.typeInfo() != null && target.typeInfo().isFunctionalInterface() && target.arrays() == 0 + && target.parameters().stream().allMatch(p -> p.wildcard() == null)) { + // return Util::parse into a Function: Kotlin types a bare reference as its own KFunction1, + // which is no Function; an argument converts by itself, a returned or assigned value needs the SAM constructor + return new OutputBuilderImpl().add(new TextImpl(KotlinTypeName.of(target.withNullable( + io.codelaser.maddi.cst.api.type.NullableState.NONNULL), q))) + .add(SymbolEnum.LEFT_PARENTHESIS).add(print(e, q)).add(SymbolEnum.RIGHT_PARENTHESIS); + } if (KotlinContext.translatingJava() && unwrap(e) instanceof MethodCall mc && target != null && !target.parameters().isEmpty() && mc.typeArguments().stream().anyMatch(KotlinTypeName::definitelyNonNull) && sameUpToStates(mc.parameterizedType(), target) && nullableArgument(mc.methodInfo())) { diff --git a/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinCalls.java b/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinCalls.java index 312122deb..6beaf9708 100644 --- a/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinCalls.java +++ b/maddi-cst-print-kotlin/src/test/java/io/codelaser/maddi/cst/print/kotlin/TestJavaToKotlinCalls.java @@ -460,6 +460,30 @@ class Search { compiles(kotlin); } + /** + * A static method reference returned, or assigned, as a java.util.function.Function: Kotlin types a bare + * {@code Util::parse} as a KFunction1, which is no Function. Passed as an argument, Kotlin converts it itself. + */ + @Test + public void staticReferenceReturnedAsFunction() { + String kotlin = kotlin(""" + package a; + import java.util.function.*; + class Util { + static int parse(String s) { return s.length(); } + static Function parser() { return Util::parse; } + static Function local() { Function f = Util::parse; return f; } + static int apply(Function f) { return f.apply("x"); } + static int use() { return apply(Util::parse); } + } + """); + contains(kotlin, "fun parser(): Function = Function(Util ::parse)"); + contains(kotlin, "= Function(Util ::parse)\nreturn f"); + // the argument as it was: Kotlin converts it to the parameter's interface + contains(kotlin, "apply(Util ::parse)"); + compiles(kotlin); + } + /** * guava's Graphs.inducedSubgraph: {@code MutableGraph subgraph = c ? b.build() : b.build()}, a builder whose * {@code build()} only the target types. The local keeps its declared type.