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<T>), asNullable(JavaType<T>), and
  asNullable(Type) to explicitly opt into nullable values
* Keep the signatures of Config.as(Class<T>) and Config.as(JavaType<T>)
  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> T Config.as(Type)` to `<T extends @nullable Object> 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<T> represents a non-null top-level type whose type arguments may be nullable
  * Restricting <T> to non-null keeps method signatures understandable for humans and tools
  * Enables full symmetry between Class<T> and JavaType<T> 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
This commit is contained in:
odenix
2026-05-19 21:27:59 +02:00
committed by GitHub
parent e34c3e8c4f
commit dc9003d0f1
6 changed files with 622 additions and 57 deletions
@@ -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> T as(Class<T> type) {
return as((Type) type);
return trustNonNull(asNullable(type));
}
@Override
public <T> T as(JavaType<T> type) {
return trustNonNull(asNullable(type));
}
@Override
public <T> T as(Type type) {
return trustNonNull(asNullable(type));
}
@Override
public <T> @Nullable T asNullable(Class<T> type) {
return mapper.map(getRawValue(), type);
}
@Override
public <T> T as(JavaType<T> javaType) {
return as(javaType.getType());
public <T> @Nullable T asNullable(JavaType<T> type) {
return mapper.map(getRawValue(), type.getType());
}
@Override
public <T extends @Nullable Object> T asNullable(Type type) {
return mapper.map(getRawValue(), type);
}
@SuppressWarnings({"unchecked", "DataFlowIssue", "NullAway"})
private static <T> T trustNonNull(@Nullable Object value) {
return (T) value;
}
protected abstract Object getRawChildValue(String property);
@@ -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.
*
* <p>To navigate to a child node, use {@link #get(String)} with the child's unqualified name.
*
* <p>To retrieve this node's value, use:
*
* <ul>
* <li>{@link #as(Class)} for non-null types.
* <li>{@link #asNullable(Class)} for nullable types.
* <li>{@link #as(JavaType)} for parameterized types, such as {@code List<@Nullable String>}.
* </ul>
*
* <p>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<String>} 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.
*
* <p>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.
*
* <p>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.
*
* <p>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}.
*
* <p>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 extends @Nullable Object> T as(Class<T> type);
@NullUnmarked
<T> T as(Class<T> 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}.
*
* <p>Note that usages of this method are not type safe.
* <p>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.
*
* <p>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 extends @Nullable Object> T as(Type type);
@NullUnmarked
<T> T as(JavaType<T> 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}.
*
* <p>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.
*
* <p>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 extends @Nullable Object> T as(JavaType<T> type);
@NullUnmarked
<T> T as(Type type);
/**
* Returns this node's value as a nullable value of the given {@link Class}.
*
* <p>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
*/
<T> @Nullable T asNullable(Class<T> type);
/**
* Returns this node's value as a nullable value of the given {@link Class}.
*
* <p>If this node's value cannot be {@code null}, use {@link #as(JavaType)} instead.
*
* <p>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> @Nullable T asNullable(JavaType<T> type);
/**
* Returns this node's value as a nullable value of the given {@link Type}.
*
* <p>If this node's value cannot be {@code null}, use {@link #as(Type)} instead.
*
* <p>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 extends @Nullable Object> T asNullable(Type type);
/**
* Decodes a config from the supplied byte array.
@@ -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<String>} can be constructed using {@code JavaType.listOf(String.class)}.
* Represents a Java type, including fully parameterized types.
*
* <p>Parameterizations of other types can be constructed using the <em>super type token</em> idiom:
* <p>This class captures complete type information that cannot be expressed with {@link Class}, for
* example {@code List<String>} or {@code Result<String, @Nullable Integer>}. It is often used with
* {@link Config#as(JavaType)}.
*
* <p>
* <p>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>}.
*
* <p>To construct a non-parameterized type, use {@link #of(Class)}.
*
* <p>To construct common parameterized types, use one of:
*
* <ul>
* <li>{@link #optionalOf(Class)}
* <li>{@link #arrayOf(Class)}
* <li>{@link #collectionOf(Class)}
* <li>{@link #iterableOf(Class)}
* <li>{@link #listOf(Class)}
* <li>{@link #setOf(Class)}
* <li>{@link #mapOf(Class, Class)}
* <li>{@link #pairOf(Class, Class)}
* </ul>
*
* <p>Apart from {@code optionalOf()}, the above methods have nullable variants:
*
* <ul>
* <li>{@link #arrayOfNullable(Class)}
* <li>{@link #collectionOfNullable(Class)}
* <li>{@link #iterableOfNullable(Class)}
* <li>{@link #listOfNullable(Class)}
* <li>{@link #setOfNullable(Class)}
* <li>{@link #mapOfNullableKeys(Class, Class)}
* <li>{@link #mapOfNullableValues(Class, Class)}
* <li>{@link #mapOfNullableKeysAndValues(Class, Class)}
* <li>{@link #pairOfNullableFirst(Class, Class)}
* <li>{@link #pairOfNullableSecond(Class, Class)}
* <li>{@link #pairOfNullableFirstAndSecond(Class, Class)}
* </ul>
*
* <p>These methods can be nested. For example, {@code optionalOf(listOfNullable(String.class))}
* represents {@code Optional<List<@Nullable String>>}.
*
* <p>To construct arbitrary parameterized types, use the <em>super-type token</em> idiom:
*
* <pre>{@code
* class Mapping<T> {} // some user-defined type
* class Result<T, U> {} // library or user-defined type
*
* Config config = ...
*
* Mapping<String> container = config.as(
* // construct super type token for Mapping<String>
* new JavaType<Mapping<String>>() {}
* );
* Result<String, @Nullable Integer> result =
* config.as(new JavaType<Result<String, @Nullable Integer>>() {});
* }</pre>
*
* @param <T> the type reified by this {@code JavaType} instance
* @param <T> the non-null type represented by this {@code JavaType}
*/
@SuppressWarnings("unused")
public class JavaType<T> {
private final Type type;
/**
* Constructs a {@code JavaType} using the super-type token idiom.
*
* <p>Subclasses must be parameterized with the desired type, for example:
*
* <pre>{@code
* new JavaType<List<@Nullable String>>() {}
* }</pre>
*
* @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<T> {
this.type = type;
}
/** Creates a {@code JavaType} for the given {@code Class}. */
/** Creates a {@code JavaType} for the given type. */
public static <T> JavaType<T> of(Class<T> type) {
return new JavaType<>(type);
}
/**
* Creates a {@code JavaType} for the given {@code Type}.
* Creates a {@code JavaType} for the given {@link Type}.
*
* <p>Note: This method is not type safe, and should be used with care.
* <p>Use this method when the target type is already available as a {@link Type}; otherwise,
* prefer {@link #of(Class)}.
*/
public static <T> JavaType<T> 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.
*
* <p>For a parameterized element type, use {@link #optionalOf(JavaType)}.
*/
public static <E> JavaType<Optional<E>> optionalOf(Class<E> 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 <E> JavaType<Optional<E>> optionalOf(JavaType<E> 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.
*
* <p>For nullable first and/or second element types, use one of the {@code pairOfNullable*}
* methods.
*
* <p>For parameterized element types, use {@link #pairOf(JavaType, JavaType)}.
*/
public static <F, S> JavaType<Pair<F, S>> pairOf(Class<F> firstType, Class<S> 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.
*
* <p>For nullable first and/or second element types, use one of the {@code pairOfNullable*}
* methods.
*/
public static <F, S> JavaType<Pair<F, S>> pairOf(JavaType<F> firstType, JavaType<S> 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.
*
* <p>For parameterized element types, use {@link #pairOfNullableFirst(JavaType, JavaType)}.
*/
public static <F, S> JavaType<Pair<@Nullable F, S>> pairOfNullableFirst(
Class<F> firstType, Class<S> 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 <F, S> JavaType<Pair<@Nullable F, S>> pairOfNullableFirst(
JavaType<F> firstType, JavaType<S> 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.
*
* <p>For parameterized element types, use {@link #pairOfNullableSecond(JavaType, JavaType)}.
*/
public static <F, S> JavaType<Pair<F, @Nullable S>> pairOfNullableSecond(
Class<F> firstType, Class<S> 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 <F, S> JavaType<Pair<F, @Nullable S>> pairOfNullableSecond(
JavaType<F> firstType, JavaType<S> 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.
*
* <p>For parameterized element types, use {@link #pairOfNullableFirstAndSecond(JavaType,
* JavaType)}.
*/
public static <F, S> JavaType<Pair<@Nullable F, @Nullable S>> pairOfNullableFirstAndSecond(
Class<F> firstType, Class<S> 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 <F, S> JavaType<Pair<@Nullable F, @Nullable S>> pairOfNullableFirstAndSecond(
JavaType<F> firstType, JavaType<S> secondType) {
return JavaType.of(Types.pairOf(firstType.type, secondType.type));
}
/**
* Creates an array type with the given non-null element type.
*
* <p>For a nullable element type, use {@link #arrayOfNullable(Class)}.
*
* <p>For a parameterized element type, use {@link #arrayOf(JavaType)}.
*/
public static <E> JavaType<E[]> arrayOf(Class<E> 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.
*
* <p>For a nullable element type, use {@link #arrayOfNullable(JavaType)}.
*/
public static <E> JavaType<E[]> arrayOf(JavaType<E> 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.
*
* <p>For a non-null element type, use {@link #arrayOf(Class)}.
*
* <p>For a parameterized element type, use {@link #arrayOfNullable(JavaType)}.
*/
public static <E> JavaType<@Nullable E[]> arrayOfNullable(Class<E> elementType) {
return JavaType.of(Types.arrayOf(elementType));
}
/**
* Creates an array type whose element type is nullable.
*
* <p>For a non-null element type, use {@link #arrayOf(JavaType)}.
*/
public static <E> JavaType<@Nullable E[]> arrayOfNullable(JavaType<E> elementType) {
return JavaType.of(Types.arrayOf(elementType.type));
}
/**
* Creates an {@link Iterable} type with the given non-null element type.
*
* <p>For a nullable element type, use {@link #iterableOfNullable(Class)}.
*
* <p>For a parameterized element type, use {@link #iterableOf(JavaType)}.
*/
public static <E> JavaType<Iterable<E>> iterableOf(Class<E> 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.
*
* <p>For a nullable element type, use {@link #iterableOfNullable(JavaType)}.
*/
public static <E> JavaType<Iterable<E>> iterableOf(JavaType<E> 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.
*
* <p>For a non-null element type, use {@link #iterableOf(Class)}.
*
* <p>For a parameterized element type, use {@link #iterableOfNullable(JavaType)}.
*/
public static <E> JavaType<Iterable<@Nullable E>> iterableOfNullable(Class<E> elementType) {
return JavaType.of(Types.iterableOf(elementType));
}
/**
* Creates an {@link Iterable} type whose element type is nullable.
*
* <p>For a non-null element type, use {@link #iterableOf(JavaType)}.
*/
public static <E> JavaType<Iterable<@Nullable E>> iterableOfNullable(JavaType<E> elementType) {
return JavaType.of(Types.iterableOf(elementType.type));
}
/**
* Creates a {@link Collection} type with the given non-null element type.
*
* <p>For a nullable element type, use {@link #collectionOfNullable(Class)}.
*
* <p>For a parameterized element type, use {@link #collectionOf(JavaType)}.
*/
public static <E> JavaType<Collection<E>> collectionOf(Class<E> 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.
*
* <p>For a nullable element type, use {@link #collectionOfNullable(JavaType)}.
*/
public static <E> JavaType<Collection<E>> collectionOf(JavaType<E> 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.
*
* <p>For a non-null element type, use {@link #collectionOf(Class)}.
*
* <p>For a parameterized element type, use {@link #collectionOfNullable(JavaType)}.
*/
public static <E> JavaType<Collection<@Nullable E>> collectionOfNullable(Class<E> elementType) {
return JavaType.of(Types.collectionOf(elementType));
}
/**
* Creates a {@link Collection} type whose element type is nullable.
*
* <p>For a non-null element type, use {@link #collectionOf(JavaType)}.
*/
public static <E> JavaType<Collection<@Nullable E>> collectionOfNullable(
JavaType<E> elementType) {
return JavaType.of(Types.collectionOf(elementType.type));
}
/**
* Creates a {@link List} type with the given non-null element type.
*
* <p>For a nullable element type, use {@link #listOfNullable(Class)}.
*
* <p>For a parameterized element type, use {@link #listOf(JavaType)}.
*/
public static <E> JavaType<List<E>> listOf(Class<E> 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.
*
* <p>For a nullable element type, use {@link #listOfNullable(JavaType)}.
*/
public static <E> JavaType<List<E>> listOf(JavaType<E> 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.
*
* <p>For a non-null element type, use {@link #listOf(Class)}.
*
* <p>For a parameterized element type, use {@link #listOfNullable(JavaType)}.
*/
public static <E> JavaType<List<@Nullable E>> listOfNullable(Class<E> elementType) {
return JavaType.of(Types.listOf(elementType));
}
/**
* Creates a {@link List} type whose element type is nullable.
*
* <p>For a non-null element type, use {@link #listOf(JavaType)}.
*/
public static <E> JavaType<List<@Nullable E>> listOfNullable(JavaType<E> elementType) {
return JavaType.of(Types.listOf(elementType.type));
}
/**
* Creates a {@link Set} type with the given non-null element type.
*
* <p>For a nullable element type, use {@link #setOfNullable(Class)}.
*
* <p>For a parameterized element type, use {@link #setOf(JavaType)}.
*/
public static <E> JavaType<Set<E>> setOf(Class<E> 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.
*
* <p>For a nullable element type, use {@link #setOfNullable(JavaType)}.
*/
public static <E> JavaType<Set<E>> setOf(JavaType<E> 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.
*
* <p>For a non-null element type, use {@link #setOf(Class)}.
*
* <p>For a parameterized element type, use {@link #setOfNullable(JavaType)}.
*/
public static <E> JavaType<Set<@Nullable E>> setOfNullable(Class<E> elementType) {
return JavaType.of(Types.setOf(elementType));
}
/**
* Creates a {@link Set} type whose element type is nullable.
*
* <p>For a non-null element type, use {@link #setOf(JavaType)}.
*/
public static <E> JavaType<Set<@Nullable E>> setOfNullable(JavaType<E> elementType) {
return JavaType.of(Types.setOf(elementType.type));
}
/**
* Creates a {@link Map} type with the given non-null key and non-null value types.
*
* <p>For nullable keys and/or values, use one of the {@code mapOfNullable*} methods.
*
* <p>For parameterized key and value types, use {@link #mapOf(JavaType, JavaType)}.
*/
public static <K, V> JavaType<Map<K, V>> mapOf(Class<K> keyType, Class<V> 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.
*
* <p>For nullable keys and/or values, use one of the {@code mapOfNullable*} methods.
*/
public static <K, V> JavaType<Map<K, V>> mapOf(JavaType<K> keyType, JavaType<V> 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.
*
* <p>For parameterized key and value types, use {@link #mapOfNullableKeys(JavaType, JavaType)}.
*/
public static <K, V> JavaType<Map<@Nullable K, V>> mapOfNullableKeys(
Class<K> keyType, Class<V> 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 <K, V> JavaType<Map<@Nullable K, V>> mapOfNullableKeys(
JavaType<K> keyType, JavaType<V> 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.
*
* <p>For parameterized key and value types, use {@link #mapOfNullableValues(JavaType, JavaType)}.
*/
public static <K, V> JavaType<Map<K, @Nullable V>> mapOfNullableValues(
Class<K> keyType, Class<V> 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 <K, V> JavaType<Map<K, @Nullable V>> mapOfNullableValues(
JavaType<K> keyType, JavaType<V> valueType) {
return JavaType.of(Types.mapOf(keyType.type, valueType.type));
}
/**
* Creates a {@link Map} type with the given nullable key and nullable value types.
*
* <p>For parameterized key and value types, use {@link #mapOfNullableKeysAndValues(JavaType,
* JavaType)}.
*/
public static <K, V> JavaType<Map<@Nullable K, @Nullable V>> mapOfNullableKeysAndValues(
Class<K> keyType, Class<V> valueType) {
return JavaType.of(Types.mapOf(keyType, valueType));
}
/** Creates a {@link Map} type with the given nullable key and nullable value types. */
public static <K, V> JavaType<Map<@Nullable K, @Nullable V>> mapOfNullableKeysAndValues(
JavaType<K> keyType, JavaType<V> valueType) {
return JavaType.of(Types.mapOf(keyType.type, valueType.type));
}
/** Returns the underlying {@link Type}. */
public Type getType() {
return type;
@@ -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();
}
}
@@ -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<String>() {});
//noinspection AssertBetweenInconvertibleTypes
assertThat(JavaType.of((Type) String.class)).isEqualTo(new JavaType<String>() {});
}
@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<Pair<String, Integer>>() {});
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<Iterable<@Nullable String>>() {});
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<Collection<@Nullable String>>() {});
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<List<@Nullable String>>() {});
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<Set<@Nullable String>>() {});
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<Pair<@Nullable String, Integer>>() {});
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<Pair<String, @Nullable Integer>>() {});
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<Pair<@Nullable String, @Nullable Integer>>() {});
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<Map<@Nullable String, URI>>() {});
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<Map<String, @Nullable URI>>() {});
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<Map<@Nullable String, @Nullable URI>>() {});
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<Map<String, List<URI>>>() {};
assertThat(javaType.getType()).isEqualTo(Types.mapOf(String.class, Types.listOf(URI.class)));
@@ -110,6 +224,7 @@ public final class JavaTypeTest {
var type2 = new JavaType<Map<String, List<URL>>>() {};
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);