From 921e7017f574bf58e54097087b24291df55c2da6 Mon Sep 17 00:00:00 2001 From: Tom Tourwe Date: Sun, 18 Oct 2026 13:25:39 +0200 Subject: [PATCH] =?UTF-8?q?Kotlin=20printer:=20a=20type-variable=20value?= =?UTF-8?q?=20looked=20up=20in=20a=20Set=20or=20Map=20i?= =?UTF-8?q?s=20not=20asserted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Java `values.contains(v)` with `Set values` and a `V v` printed as `values.contains(v!!)` (and `names.get(v!!)`). Java returns false (or null) for a null v; the Kotlin threw a NullPointerException, and nothing reported the change. The lookup check recognised an element type that is a class, or another type variable than the argument's, but not the same one. Now: `values.contains(v)`, `names.get(v) != null`; `values.add(v)` is unchanged. Test: TestJavaToKotlinStructure.sameTypeVariableLookupNotAsserted (output compiles with kotlinc). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tom Tourwe --- .../print/kotlin/KotlinExpressionPrinter.java | 8 ++++++- .../kotlin/TestJavaToKotlinStructure.java | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) 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..4c9bf3d2c 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 @@ -783,7 +783,7 @@ static OutputBuilder arguments(List args, io.codelaser.maddi.cst.api lookedUp = null; } if (lookedUp != null) { - if (lookedUp.typeParameter() != null + if (lookedUp.typeParameter() != null && lookedUp.typeParameter() != args.get(i).parameterizedType().typeParameter() || unwrap(args.get(i)).parameterizedType().typeInfo() != lookedUp.typeInfo() && KotlinNullability.nullableInKotlin(args.get(i)) // map.entries.contains(immutableEntry(key, value)) into a Set>: the extension's @@ -1261,6 +1261,12 @@ private static ParameterizedType nullableLookup(io.codelaser.maddi.cst.api.info. } ParameterizedType argumentType = argument.parameterizedType(); if (typeVariableLookup(element, argumentType)) return element; + if (element != null && argumentType != null && element.typeParameter() != null && element.arrays() == 0 + && element.wildcard() == null && argumentType.arrays() == 0 + && argumentType.typeParameter() == element.typeParameter()) { + // values.contains(v) with a V into a Set: Kotlin's member takes the V as it is, null included + return element; + } boolean found = element != null && argumentType != null && element.arrays() == argumentType.arrays() && element.wildcard() == null && element.typeInfo() != null && (element.typeInfo() == argumentType.typeInfo() 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..9596717b5 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 @@ -1818,6 +1818,30 @@ boolean has(Object key, Object value) { compiles(kotlin); } + /** + * A V looked up in a Set or a Map: Java's contains and get take an Object, and a null V finds + * nothing (false, null). Kotlin's member takes the V itself, which may be null: no `!!`, which would throw. + */ + @Test + public void sameTypeVariableLookupNotAsserted() { + String kotlin = kotlin(""" + package a; + import java.util.*; + class Look { + private final Set values = new HashSet<>(); + private final Map names = new HashMap<>(); + boolean hasValue(V v) { return values.contains(v); } + boolean named(V v) { return names.get(v) != null; } + boolean add(V v) { return values.add(v); } + } + """); + contains(kotlin, "values.contains(v)"); + contains(kotlin, "names.get(v) != null"); + contains(kotlin, "values.add(v)"); + assertFalse(kotlin.contains("v!!"), () -> "a null v finds nothing in Java; the printed Kotlin asserts it non-null:\n" + kotlin); + compiles(kotlin); + } + /** * guava's collectors: {@code Collector.of(Acc::create, Acc::add, Acc::combine, Acc::result)} on a generic class * of ours. Kotlin wants the receiver's type arguments on an unbound reference ({@code Acc::add}, from the