From dc9003d0f192e2e838bee09bcabff8078b58b60b Mon Sep 17 00:00:00 2001 From: odenix Date: Tue, 19 May 2026 21:27:59 +0200 Subject: [PATCH] pkl-config-java: Refine nullness handling in Config and JavaType (#1544) Motivation: Config.as() causes nullness warnings when its result is intentionally assigned to a non-null variable Changes: * Introduce Config.asNullable(Class), asNullable(JavaType), and asNullable(Type) to explicitly opt into nullable values * Keep the signatures of Config.as(Class) and Config.as(JavaType) unchanged from 0.31 by adding @NullUnmarked * This gives users time to migrate from as() to asNullable() where appropriate * Avoids introducing new spurious warnings * Change ` T Config.as(Type)` to ` T Config.as(Type)` * This overload is typically used by reflective code such as pkl-config-kotlin's Config.to() rather than directly by user code * Clarify that JavaType represents a non-null top-level type whose type arguments may be nullable * Restricting to non-null keeps method signatures understandable for humans and tools * Enables full symmetry between Class and JavaType overloads in Config and JavaType * Enables future non-null runtime checks in both Config.as() overloads * Simplify construction of `JavaType`s with nullable type arguments * Add ofNullable() variants for most factory methods, e.g., JavaType.listOfNullable() * Overhaul Javadoc of Config and JavaType Result: * Clear separation between accessing nullable and non-null values * Config.as() is used for the common non-null case * Config.as() can perform non-null runtime checks in a future release (breaking change) * More ergonomic construction of types with nullable type arguments * More detailed and consistent documentation --- .../org/pkl/config/java/AbstractConfig.java | 29 +- .../main/java/org/pkl/config/java/Config.java | 102 ++++- .../java/org/pkl/config/java/JavaType.java | 391 ++++++++++++++++-- .../pkl/config/java/ConfigEvaluatorTest.java | 29 ++ .../org/pkl/config/java/JavaTypeTest.java | 117 +++++- .../org/pkl/config/kotlin/ConfigExtensions.kt | 11 +- 6 files changed, 622 insertions(+), 57 deletions(-) diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/AbstractConfig.java b/pkl-config-java/src/main/java/org/pkl/config/java/AbstractConfig.java index e7165119..df0761a7 100644 --- a/pkl-config-java/src/main/java/org/pkl/config/java/AbstractConfig.java +++ b/pkl-config-java/src/main/java/org/pkl/config/java/AbstractConfig.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.pkl.config.java; import java.lang.reflect.Type; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.pkl.config.java.mapper.ValueMapper; import org.pkl.core.Composite; @@ -49,17 +50,37 @@ abstract class AbstractConfig implements Config { @Override public T as(Class type) { - return as((Type) type); + return trustNonNull(asNullable(type)); + } + + @Override + public T as(JavaType type) { + return trustNonNull(asNullable(type)); } @Override public T as(Type type) { + return trustNonNull(asNullable(type)); + } + + @Override + public @Nullable T asNullable(Class type) { return mapper.map(getRawValue(), type); } @Override - public T as(JavaType javaType) { - return as(javaType.getType()); + public @Nullable T asNullable(JavaType type) { + return mapper.map(getRawValue(), type.getType()); + } + + @Override + public T asNullable(Type type) { + return mapper.map(getRawValue(), type); + } + + @SuppressWarnings({"unchecked", "DataFlowIssue", "NullAway"}) + private static T trustNonNull(@Nullable Object value) { + return (T) value; } protected abstract Object getRawChildValue(String property); diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/Config.java b/pkl-config-java/src/main/java/org/pkl/config/java/Config.java index bd119e15..737bb3ca 100644 --- a/pkl-config-java/src/main/java/org/pkl/config/java/Config.java +++ b/pkl-config-java/src/main/java/org/pkl/config/java/Config.java @@ -17,60 +17,130 @@ package org.pkl.config.java; import java.io.InputStream; import java.lang.reflect.Type; +import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; import org.pkl.config.java.mapper.ConversionException; import org.pkl.config.java.mapper.ValueMapper; -import org.pkl.core.Evaluator; /** - * A root, intermediate, or leaf node in a configuration tree. Child nodes can be obtained by name - * using {@link #get(String)}. To consume the node's composite or scalar value, convert the value to - * the desired Java type, using one of the provided {@link #as} methods. + * A root, intermediate, or leaf node in a configuration tree. + * + *

To navigate to a child node, use {@link #get(String)} with the child's unqualified name. + * + *

To retrieve this node's value, use: + * + *

    + *
  • {@link #as(Class)} for non-null types. + *
  • {@link #asNullable(Class)} for nullable types. + *
  • {@link #as(JavaType)} for parameterized types, such as {@code List<@Nullable String>}. + *
+ * + *

Whether a method can return null depends on the method and target type used. For example, + * {@code asNullable(String.class)} can return {@code null}, while {@code + * as(JavaType.listOfNullable(String.class))} can return a {@code List} with nullable + * elements. These nullness rules are for static analysis tools such as IntelliJ IDEA and NullAway + * and are not enforced at runtime. */ @SuppressWarnings({"DeprecatedIsStillUsed"}) public interface Config { /** - * The dot-separated name of this node. For example, the node reached using {@code - * rootNode.get("foo").get("bar")} has qualified name {@code foo.bar}. Returns the empty String - * for the root node itself. + * Returns the qualified name of this node, or the empty string if this is the root node. + * + *

The qualified name is formed by joining child names using {@code '.'}. For example, {@code + * rootNode.get("foo").get("bar").getQualifiedName()} returns {@code "foo.bar"}. */ String getQualifiedName(); /** - * The raw value of this node, as provided by {@link Evaluator}. Typically, a node's value is not - * consumed directly, but converted to the desired Java type using {@link #as}. + * Returns the underlying value of this node. + * + *

This value is typically accessed indirectly via {@link #as(Class)}, {@link + * #asNullable(Class)}, or {@link #as(JavaType)}. */ Object getRawValue(); /** * Returns the child node with the given unqualified name. * + *

For example, {@code get("foo").get("bar")} returns the child named {@code "bar"} of the + * child named {@code "foo"}. Passing a qualified name to this method, as in {@code + * get("foo.bar")}, is not supported. + * * @throws NoSuchChildException if a child with the given name does not exist */ Config get(String childName); /** - * Converts this node's value to the given {@link Class}. + * Returns this node's value as a non-null value of the given {@link Class}. + * + *

If this node's value may be {@code null}, use {@link #asNullable(Class)} instead. In a + * future version, this method will perform a non-null check. * * @throws ConversionException if the value cannot be converted to the given type */ - T as(Class type); + @NullUnmarked + T as(Class type); /** - * Converts this node's value to the given {@link Type}. + * Returns this node's value as a non-null value of the given {@link JavaType}. * - *

Note that usages of this method are not type safe. + *

If this node's value may be {@code null}, use {@link #asNullable(JavaType)} instead. In a + * future version, this method will perform a non-null check. + * + *

Use this method when converting to a parameterized type, such as {@code List<@Nullable + * String>}. * * @throws ConversionException if the value cannot be converted to the given type */ - T as(Type type); + @NullUnmarked + T as(JavaType type); /** - * Converts this node's value to the given {@link JavaType}. + * Returns this node's value as a non-null value of the given {@link Type}. + * + *

If this node's value may be {@code null}, use {@link #asNullable(Type)} instead. In a future + * version, this method will perform a non-null check. + * + *

Use this method when the target type is already available as a {@link Type}; otherwise, + * prefer {@link #as(Class)} or {@link #as(JavaType)}. * * @throws ConversionException if the value cannot be converted to the given type */ - T as(JavaType type); + @NullUnmarked + T as(Type type); + + /** + * Returns this node's value as a nullable value of the given {@link Class}. + * + *

If this node's value cannot be {@code null}, use {@link #as(Class)} instead. + * + * @throws ConversionException if the value cannot be converted to the given type + */ + @Nullable T asNullable(Class type); + + /** + * Returns this node's value as a nullable value of the given {@link Class}. + * + *

If this node's value cannot be {@code null}, use {@link #as(JavaType)} instead. + * + *

Use this method when converting to a parameterized type, such as {@code List<@Nullable + * String>}. + * + * @throws ConversionException if the value cannot be converted to the given type + */ + @Nullable T asNullable(JavaType type); + + /** + * Returns this node's value as a nullable value of the given {@link Type}. + * + *

If this node's value cannot be {@code null}, use {@link #as(Type)} instead. + * + *

Use this method when the target type is already available as a {@link Type}; otherwise, + * prefer {@link #asNullable(Class)} or {@link #as(JavaType)}. + * + * @throws ConversionException if the value cannot be converted to the given type + */ + T asNullable(Type type); /** * Decodes a config from the supplied byte array. diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/JavaType.java b/pkl-config-java/src/main/java/org/pkl/config/java/JavaType.java index f36e6992..071b14dc 100644 --- a/pkl-config-java/src/main/java/org/pkl/config/java/JavaType.java +++ b/pkl-config-java/src/main/java/org/pkl/config/java/JavaType.java @@ -23,30 +23,77 @@ import org.pkl.config.java.mapper.Types; import org.pkl.core.Pair; /** - * Runtime representation of a possibly parameterized Java type. Factory methods are provided to - * ease construction of commonly used Java standard library types. For example, a {@code JavaType} - * for {@code List} can be constructed using {@code JavaType.listOf(String.class)}. + * Represents a Java type, including fully parameterized types. * - *

Parameterizations of other types can be constructed using the super type token idiom: + *

This class captures complete type information that cannot be expressed with {@link Class}, for + * example {@code List} or {@code Result}. It is often used with + * {@link Config#as(JavaType)}. * - *

+ *

A {@code JavaType} always represents a non-null top-level type, but its type arguments may be + * nullable. For example, {@code listOfNullable(String.class)} represents {@code List<@Nullable + * String>}. + * + *

To construct a non-parameterized type, use {@link #of(Class)}. + * + *

To construct common parameterized types, use one of: + * + *

    + *
  • {@link #optionalOf(Class)} + *
  • {@link #arrayOf(Class)} + *
  • {@link #collectionOf(Class)} + *
  • {@link #iterableOf(Class)} + *
  • {@link #listOf(Class)} + *
  • {@link #setOf(Class)} + *
  • {@link #mapOf(Class, Class)} + *
  • {@link #pairOf(Class, Class)} + *
+ * + *

Apart from {@code optionalOf()}, the above methods have nullable variants: + * + *

    + *
  • {@link #arrayOfNullable(Class)} + *
  • {@link #collectionOfNullable(Class)} + *
  • {@link #iterableOfNullable(Class)} + *
  • {@link #listOfNullable(Class)} + *
  • {@link #setOfNullable(Class)} + *
  • {@link #mapOfNullableKeys(Class, Class)} + *
  • {@link #mapOfNullableValues(Class, Class)} + *
  • {@link #mapOfNullableKeysAndValues(Class, Class)} + *
  • {@link #pairOfNullableFirst(Class, Class)} + *
  • {@link #pairOfNullableSecond(Class, Class)} + *
  • {@link #pairOfNullableFirstAndSecond(Class, Class)} + *
+ * + *

These methods can be nested. For example, {@code optionalOf(listOfNullable(String.class))} + * represents {@code Optional>}. + * + *

To construct arbitrary parameterized types, use the super-type token idiom: * *

{@code
- * class Mapping {} // some user-defined type
+ * class Result {} // library or user-defined type
+ *
  * Config config = ...
  *
- * Mapping container = config.as(
- *   // construct super type token for Mapping
- *   new JavaType>() {}
- * );
+ * Result result =
+ *     config.as(new JavaType>() {});
  * }
* - * @param the type reified by this {@code JavaType} instance + * @param the non-null type represented by this {@code JavaType} */ -@SuppressWarnings("unused") public class JavaType { private final Type type; + /** + * Constructs a {@code JavaType} using the super-type token idiom. + * + *

Subclasses must be parameterized with the desired type, for example: + * + *

{@code
+   * new JavaType>() {}
+   * }
+ * + * @throws IllegalStateException if this instance is not parameterized + */ protected JavaType() { var superclass = getClass().getGenericSuperclass(); if (!(superclass instanceof ParameterizedType parameterizedType)) { @@ -59,100 +106,382 @@ public class JavaType { this.type = type; } - /** Creates a {@code JavaType} for the given {@code Class}. */ + /** Creates a {@code JavaType} for the given type. */ public static JavaType of(Class type) { return new JavaType<>(type); } /** - * Creates a {@code JavaType} for the given {@code Type}. + * Creates a {@code JavaType} for the given {@link Type}. * - *

Note: This method is not type safe, and should be used with care. + *

Use this method when the target type is already available as a {@link Type}; otherwise, + * prefer {@link #of(Class)}. */ public static JavaType of(Type type) { return new JavaType<>(type); } - /** Creates an {@link Optional} type with the given element type. */ + /** + * Creates an {@link Optional} type with the given non-null element type. + * + *

For a parameterized element type, use {@link #optionalOf(JavaType)}. + */ public static JavaType> optionalOf(Class elementType) { return JavaType.of(Types.optionalOf(elementType)); } - /** Creates an {@link Optional} type with the given element type. */ + /** Creates an {@link Optional} type with the given non-null element type. */ public static JavaType> optionalOf(JavaType elementType) { return JavaType.of(Types.optionalOf(elementType.type)); } - /** Creates a {@link Pair} type with the given first and second element types. */ + /** + * Creates a {@link Pair} type with the given non-null first and non-null second element types. + * + *

For nullable first and/or second element types, use one of the {@code pairOfNullable*} + * methods. + * + *

For parameterized element types, use {@link #pairOf(JavaType, JavaType)}. + */ public static JavaType> pairOf(Class firstType, Class secondType) { return JavaType.of(Types.pairOf(firstType, secondType)); } - /** Creates a {@link Pair} type with the given first and second element types. */ + /** + * Creates a {@link Pair} type with the given non-null first and non-null second element types. + * + *

For nullable first and/or second element types, use one of the {@code pairOfNullable*} + * methods. + */ public static JavaType> pairOf(JavaType firstType, JavaType secondType) { return JavaType.of(Types.pairOf(firstType.type, secondType.type)); } - /** Creates an array type with the given element type. */ + /** + * Creates a {@link Pair} type with the given nullable first and non-null second element types. + * + *

For parameterized element types, use {@link #pairOfNullableFirst(JavaType, JavaType)}. + */ + public static JavaType> pairOfNullableFirst( + Class firstType, Class secondType) { + return JavaType.of(Types.pairOf(firstType, secondType)); + } + + /** + * Creates a {@link Pair} type with the given nullable first and non-null second element types. + */ + public static JavaType> pairOfNullableFirst( + JavaType firstType, JavaType secondType) { + return JavaType.of(Types.pairOf(firstType.type, secondType.type)); + } + + /** + * Creates a {@link Pair} type with the given non-null first and nullable second element types. + * + *

For parameterized element types, use {@link #pairOfNullableSecond(JavaType, JavaType)}. + */ + public static JavaType> pairOfNullableSecond( + Class firstType, Class secondType) { + return JavaType.of(Types.pairOf(firstType, secondType)); + } + + /** + * Creates a {@link Pair} type with the given non-null first and nullable second element types. + */ + public static JavaType> pairOfNullableSecond( + JavaType firstType, JavaType secondType) { + return JavaType.of(Types.pairOf(firstType.type, secondType.type)); + } + + /** + * Creates a {@link Pair} type with the given nullable first and nullable second element types. + * + *

For parameterized element types, use {@link #pairOfNullableFirstAndSecond(JavaType, + * JavaType)}. + */ + public static JavaType> pairOfNullableFirstAndSecond( + Class firstType, Class secondType) { + return JavaType.of(Types.pairOf(firstType, secondType)); + } + + /** + * Creates a {@link Pair} type with the given nullable first and nullable second element types. + */ + public static JavaType> pairOfNullableFirstAndSecond( + JavaType firstType, JavaType secondType) { + return JavaType.of(Types.pairOf(firstType.type, secondType.type)); + } + + /** + * Creates an array type with the given non-null element type. + * + *

For a nullable element type, use {@link #arrayOfNullable(Class)}. + * + *

For a parameterized element type, use {@link #arrayOf(JavaType)}. + */ public static JavaType arrayOf(Class elementType) { return JavaType.of(Types.arrayOf(elementType)); } - /** Creates an array type with the given element type. */ + /** + * Creates an array type with the given non-null element type. + * + *

For a nullable element type, use {@link #arrayOfNullable(JavaType)}. + */ public static JavaType arrayOf(JavaType elementType) { return JavaType.of(Types.arrayOf(elementType.type)); } - /** Creates an {@link Iterable} type with the given element type. */ + /** + * Creates an array type whose element type is nullable. + * + *

For a non-null element type, use {@link #arrayOf(Class)}. + * + *

For a parameterized element type, use {@link #arrayOfNullable(JavaType)}. + */ + public static JavaType<@Nullable E[]> arrayOfNullable(Class elementType) { + return JavaType.of(Types.arrayOf(elementType)); + } + + /** + * Creates an array type whose element type is nullable. + * + *

For a non-null element type, use {@link #arrayOf(JavaType)}. + */ + public static JavaType<@Nullable E[]> arrayOfNullable(JavaType elementType) { + return JavaType.of(Types.arrayOf(elementType.type)); + } + + /** + * Creates an {@link Iterable} type with the given non-null element type. + * + *

For a nullable element type, use {@link #iterableOfNullable(Class)}. + * + *

For a parameterized element type, use {@link #iterableOf(JavaType)}. + */ public static JavaType> iterableOf(Class elementType) { return JavaType.of(Types.iterableOf(elementType)); } - /** Creates an {@link Iterable} type with the given element type. */ + /** + * Creates an {@link Iterable} type with the given non-null element type. + * + *

For a nullable element type, use {@link #iterableOfNullable(JavaType)}. + */ public static JavaType> iterableOf(JavaType elementType) { return JavaType.of(Types.iterableOf(elementType.type)); } - /** Creates a {@link Collection} type with the given element type. */ + /** + * Creates an {@link Iterable} type whose element type is nullable. + * + *

For a non-null element type, use {@link #iterableOf(Class)}. + * + *

For a parameterized element type, use {@link #iterableOfNullable(JavaType)}. + */ + public static JavaType> iterableOfNullable(Class elementType) { + return JavaType.of(Types.iterableOf(elementType)); + } + + /** + * Creates an {@link Iterable} type whose element type is nullable. + * + *

For a non-null element type, use {@link #iterableOf(JavaType)}. + */ + public static JavaType> iterableOfNullable(JavaType elementType) { + return JavaType.of(Types.iterableOf(elementType.type)); + } + + /** + * Creates a {@link Collection} type with the given non-null element type. + * + *

For a nullable element type, use {@link #collectionOfNullable(Class)}. + * + *

For a parameterized element type, use {@link #collectionOf(JavaType)}. + */ public static JavaType> collectionOf(Class elementType) { return JavaType.of(Types.collectionOf(elementType)); } - /** Creates a {@link Collection} type with the given element type. */ + /** + * Creates a {@link Collection} type with the given non-null element type. + * + *

For a nullable element type, use {@link #collectionOfNullable(JavaType)}. + */ public static JavaType> collectionOf(JavaType elementType) { return JavaType.of(Types.collectionOf(elementType.type)); } - /** Creates a {@link List} type with the given element type. */ + /** + * Creates a {@link Collection} type whose element type is nullable. + * + *

For a non-null element type, use {@link #collectionOf(Class)}. + * + *

For a parameterized element type, use {@link #collectionOfNullable(JavaType)}. + */ + public static JavaType> collectionOfNullable(Class elementType) { + return JavaType.of(Types.collectionOf(elementType)); + } + + /** + * Creates a {@link Collection} type whose element type is nullable. + * + *

For a non-null element type, use {@link #collectionOf(JavaType)}. + */ + public static JavaType> collectionOfNullable( + JavaType elementType) { + return JavaType.of(Types.collectionOf(elementType.type)); + } + + /** + * Creates a {@link List} type with the given non-null element type. + * + *

For a nullable element type, use {@link #listOfNullable(Class)}. + * + *

For a parameterized element type, use {@link #listOf(JavaType)}. + */ public static JavaType> listOf(Class elementType) { return JavaType.of(Types.listOf(elementType)); } - /** Creates a {@link List} type with the given element type. */ + /** + * Creates a {@link List} type with the given non-null element type. + * + *

For a nullable element type, use {@link #listOfNullable(JavaType)}. + */ public static JavaType> listOf(JavaType elementType) { return JavaType.of(Types.listOf(elementType.type)); } - /** Creates a {@link Set} type with the given element type. */ + /** + * Creates a {@link List} type whose element type is nullable. + * + *

For a non-null element type, use {@link #listOf(Class)}. + * + *

For a parameterized element type, use {@link #listOfNullable(JavaType)}. + */ + public static JavaType> listOfNullable(Class elementType) { + return JavaType.of(Types.listOf(elementType)); + } + + /** + * Creates a {@link List} type whose element type is nullable. + * + *

For a non-null element type, use {@link #listOf(JavaType)}. + */ + public static JavaType> listOfNullable(JavaType elementType) { + return JavaType.of(Types.listOf(elementType.type)); + } + + /** + * Creates a {@link Set} type with the given non-null element type. + * + *

For a nullable element type, use {@link #setOfNullable(Class)}. + * + *

For a parameterized element type, use {@link #setOf(JavaType)}. + */ public static JavaType> setOf(Class elementType) { return JavaType.of(Types.setOf(elementType)); } - /** Creates a {@link Set} type with the given element type. */ + /** + * Creates a {@link Set} type with the given non-null element type. + * + *

For a nullable element type, use {@link #setOfNullable(JavaType)}. + */ public static JavaType> setOf(JavaType elementType) { return JavaType.of(Types.setOf(elementType.type)); } - /** Creates a {@link Map} type with the given key and value types. */ + /** + * Creates a {@link Set} type whose element type is nullable. + * + *

For a non-null element type, use {@link #setOf(Class)}. + * + *

For a parameterized element type, use {@link #setOfNullable(JavaType)}. + */ + public static JavaType> setOfNullable(Class elementType) { + return JavaType.of(Types.setOf(elementType)); + } + + /** + * Creates a {@link Set} type whose element type is nullable. + * + *

For a non-null element type, use {@link #setOf(JavaType)}. + */ + public static JavaType> setOfNullable(JavaType elementType) { + return JavaType.of(Types.setOf(elementType.type)); + } + + /** + * Creates a {@link Map} type with the given non-null key and non-null value types. + * + *

For nullable keys and/or values, use one of the {@code mapOfNullable*} methods. + * + *

For parameterized key and value types, use {@link #mapOf(JavaType, JavaType)}. + */ public static JavaType> mapOf(Class keyType, Class valueType) { return JavaType.of(Types.mapOf(keyType, valueType)); } - /** Creates a {@link Map} type with the given key and value types. */ + /** + * Creates a {@link Map} type with the given non-null key and non-null value types. + * + *

For nullable keys and/or values, use one of the {@code mapOfNullable*} methods. + */ public static JavaType> mapOf(JavaType keyType, JavaType valueType) { return JavaType.of(Types.mapOf(keyType.type, valueType.type)); } + /** + * Creates a {@link Map} type with the given nullable key and non-null value types. + * + *

For parameterized key and value types, use {@link #mapOfNullableKeys(JavaType, JavaType)}. + */ + public static JavaType> mapOfNullableKeys( + Class keyType, Class valueType) { + return JavaType.of(Types.mapOf(keyType, valueType)); + } + + /** Creates a {@link Map} type with the given nullable key and non-null value types. */ + public static JavaType> mapOfNullableKeys( + JavaType keyType, JavaType valueType) { + return JavaType.of(Types.mapOf(keyType.type, valueType.type)); + } + + /** + * Creates a {@link Map} type with the given non-null key and nullable value types. + * + *

For parameterized key and value types, use {@link #mapOfNullableValues(JavaType, JavaType)}. + */ + public static JavaType> mapOfNullableValues( + Class keyType, Class valueType) { + return JavaType.of(Types.mapOf(keyType, valueType)); + } + + /** Creates a {@link Map} type with the given non-null key and nullable value types. */ + public static JavaType> mapOfNullableValues( + JavaType keyType, JavaType valueType) { + return JavaType.of(Types.mapOf(keyType.type, valueType.type)); + } + + /** + * Creates a {@link Map} type with the given nullable key and nullable value types. + * + *

For parameterized key and value types, use {@link #mapOfNullableKeysAndValues(JavaType, + * JavaType)}. + */ + public static JavaType> mapOfNullableKeysAndValues( + Class keyType, Class valueType) { + return JavaType.of(Types.mapOf(keyType, valueType)); + } + + /** Creates a {@link Map} type with the given nullable key and nullable value types. */ + public static JavaType> mapOfNullableKeysAndValues( + JavaType keyType, JavaType valueType) { + return JavaType.of(Types.mapOf(keyType.type, valueType.type)); + } + /** Returns the underlying {@link Type}. */ public Type getType() { return type; diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorTest.java index c1b99380..3ef2644c 100644 --- a/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorTest.java +++ b/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorTest.java @@ -17,6 +17,8 @@ package org.pkl.config.java; import static org.assertj.core.api.Assertions.assertThat; +import java.lang.reflect.Type; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.pkl.core.ModuleSource; @@ -50,4 +52,31 @@ public final class ConfigEvaluatorTest extends AbstractConfigTest { var address = addressConfig.as(Address.class); assertThat(address.street).isEqualTo("Fuzzy St."); } + + @Test + public void asNullableWithClass() { + var mod = evaluator.evaluate(ModuleSource.text("nullValue = null; strValue = \"Bob\"")); + @Nullable String nullValue = mod.get("nullValue").asNullable(String.class); + @Nullable String strValue = mod.get("strValue").asNullable(String.class); + assertThat(nullValue).isNull(); + assertThat(strValue).isNotNull(); + } + + @Test + public void asNullableWithType() { + var mod = evaluator.evaluate(ModuleSource.text("nullValue = null; strValue = \"Bob\"")); + @Nullable String nullValue = mod.get("nullValue").asNullable((Type) String.class); + @Nullable String strValue = mod.get("strValue").asNullable((Type) String.class); + assertThat(nullValue).isNull(); + assertThat(strValue).isNotNull(); + } + + @Test + public void asNullableWithJavaType() { + var mod = evaluator.evaluate(ModuleSource.text("nullValue = null; strValue = \"Bob\"")); + @Nullable String nullValue = mod.get("nullValue").asNullable(JavaType.of(String.class)); + @Nullable String strValue = mod.get("strValue").asNullable(JavaType.of(String.class)); + assertThat(nullValue).isNull(); + assertThat(strValue).isNotNull(); + } } diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/JavaTypeTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/JavaTypeTest.java index d36a8d4b..170af281 100644 --- a/pkl-config-java/src/test/java/org/pkl/config/java/JavaTypeTest.java +++ b/pkl-config-java/src/test/java/org/pkl/config/java/JavaTypeTest.java @@ -17,15 +17,25 @@ package org.pkl.config.java; import static org.assertj.core.api.Assertions.*; +import java.lang.reflect.Type; import java.net.URI; import java.net.URL; import java.nio.file.Path; import java.util.*; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.pkl.config.java.mapper.Reflection; import org.pkl.config.java.mapper.Types; +import org.pkl.core.Pair; public final class JavaTypeTest { + @Test + public void constructSimpleType() { + assertThat(JavaType.of(String.class)).isEqualTo(new JavaType() {}); + //noinspection AssertBetweenInconvertibleTypes + assertThat(JavaType.of((Type) String.class)).isEqualTo(new JavaType() {}); + } + @Test public void constructOptionalType() { var type = JavaType.optionalOf(String.class); @@ -34,6 +44,14 @@ public final class JavaTypeTest { assertThat(Reflection.toRawType(type.getType())).isEqualTo(Optional.class); } + @Test + public void constructPairType() { + var type = JavaType.pairOf(String.class, Integer.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type) + .isEqualTo(JavaType.pairOf(JavaType.of(String.class), JavaType.of(Integer.class))); + } + @Test public void constructArrayType() { var type = JavaType.arrayOf(String.class); @@ -42,6 +60,14 @@ public final class JavaTypeTest { assertThat(Reflection.toRawType(type.getType()).isArray()).isTrue(); } + @Test + public void constructArrayOfNullableType() { + var type = JavaType.arrayOfNullable(String.class); + assertThat(type).isEqualTo(new JavaType<@Nullable String[]>() {}); + assertThat(type).isEqualTo(JavaType.arrayOfNullable(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType()).isArray()).isTrue(); + } + @Test public void constructIterableType() { var type = JavaType.iterableOf(String.class); @@ -50,6 +76,14 @@ public final class JavaTypeTest { assertThat(Reflection.toRawType(type.getType())).isEqualTo(Iterable.class); } + @Test + public void constructIterableOfNullableType() { + var type = JavaType.iterableOfNullable(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.iterableOfNullable(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Iterable.class); + } + @Test public void constructCollectionType() { var type = JavaType.collectionOf(String.class); @@ -58,6 +92,14 @@ public final class JavaTypeTest { assertThat(Reflection.toRawType(type.getType())).isEqualTo(Collection.class); } + @Test + public void constructCollectionOfNullableType() { + var type = JavaType.collectionOfNullable(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.collectionOfNullable(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Collection.class); + } + @Test public void constructListType() { var type = JavaType.listOf(String.class); @@ -66,6 +108,14 @@ public final class JavaTypeTest { assertThat(Reflection.toRawType(type.getType())).isEqualTo(List.class); } + @Test + public void constructListOfNullableType() { + var type = JavaType.listOfNullable(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.listOfNullable(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(List.class); + } + @Test public void constructSetType() { var type = JavaType.setOf(String.class); @@ -74,6 +124,14 @@ public final class JavaTypeTest { assertThat(Reflection.toRawType(type.getType())).isEqualTo(Set.class); } + @Test + public void constructSetOfNullableType() { + var type = JavaType.setOfNullable(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.setOfNullable(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Set.class); + } + @Test public void constructMapType() { var type = JavaType.mapOf(String.class, URI.class); @@ -83,7 +141,63 @@ public final class JavaTypeTest { } @Test - public void usageAsTypeToken() { + public void constructPairOfNullableFirstType() { + var type = JavaType.pairOfNullableFirst(String.class, Integer.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type) + .isEqualTo( + JavaType.pairOfNullableFirst(JavaType.of(String.class), JavaType.of(Integer.class))); + } + + @Test + public void constructPairOfNullableSecondType() { + var type = JavaType.pairOfNullableSecond(String.class, Integer.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type) + .isEqualTo( + JavaType.pairOfNullableSecond(JavaType.of(String.class), JavaType.of(Integer.class))); + } + + @Test + public void constructPairOfNullableFirstAndSecondType() { + var type = JavaType.pairOfNullableFirstAndSecond(String.class, Integer.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type) + .isEqualTo( + JavaType.pairOfNullableFirstAndSecond( + JavaType.of(String.class), JavaType.of(Integer.class))); + } + + @Test + public void constructMapOfNullableKeysType() { + var type = JavaType.mapOfNullableKeys(String.class, URI.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type) + .isEqualTo(JavaType.mapOfNullableKeys(JavaType.of(String.class), JavaType.of(URI.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Map.class); + } + + @Test + public void constructMapOfNullableValuesType() { + var type = JavaType.mapOfNullableValues(String.class, URI.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type) + .isEqualTo(JavaType.mapOfNullableValues(JavaType.of(String.class), JavaType.of(URI.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Map.class); + } + + @Test + public void constructMapOfNullableKeysAndValuesType() { + var type = JavaType.mapOfNullableKeysAndValues(String.class, URI.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type) + .isEqualTo( + JavaType.mapOfNullableKeysAndValues(JavaType.of(String.class), JavaType.of(URI.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Map.class); + } + + @Test + public void constructTypeToken() { var javaType = new JavaType>>() {}; assertThat(javaType.getType()).isEqualTo(Types.mapOf(String.class, Types.listOf(URI.class))); @@ -110,6 +224,7 @@ public final class JavaTypeTest { var type2 = new JavaType>>() {}; var type3 = JavaType.of(Types.mapOf(String.class, Types.listOf(Path.class))); + //noinspection AssertBetweenInconvertibleTypes assertThat(type2).isNotEqualTo(type1); assertThat(type3).isNotEqualTo(type1); assertThat(type2).isNotEqualTo(type3); diff --git a/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/ConfigExtensions.kt b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/ConfigExtensions.kt index 44f26f81..21cbc3c9 100644 --- a/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/ConfigExtensions.kt +++ b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/ConfigExtensions.kt @@ -40,14 +40,15 @@ import org.pkl.config.kotlin.mapper.KotlinConverterFactories * `as(JavaType.listOf(String::class.java))` */ inline fun Config.to(): T { - val result = `as`(typeOf().javaType) - if (result == null && null !is T) { - throw ConversionException( + if (null is T) { + return asNullable(typeOf().javaType) + } + + return `as`(typeOf().javaType) + ?: throw ConversionException( "Expected a non-null value but got `null`. " + "To allow null values, convert to a nullable Kotlin type, for example `String?`." ) - } - return result } /**