pkl-executor: Migrate nullness to jSpecify (#1527)

Annotating SPI classes is binary compatible (forward and backward).
This commit is contained in:
odenix
2026-04-16 23:02:40 +01:00
committed by GitHub
parent 03a641354e
commit 8103b7759f
12 changed files with 143 additions and 102 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.
@@ -25,6 +25,7 @@ import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.pkl.executor.spi.v1.ExecutorSpi;
import org.pkl.executor.spi.v1.ExecutorSpiException;
import org.slf4j.Logger;
@@ -60,8 +61,7 @@ final class EmbeddedExecutor implements Executor {
Version requestedVersion = null;
PklDistribution distribution = null;
String output = null;
RuntimeException exception = null;
String output;
try {
if (!Files.isRegularFile(modulePath)) {
@@ -76,29 +76,33 @@ final class EmbeddedExecutor implements Executor {
// (but not any modules imported by it) and only requires parsing (but not evaluating) the
// module.
requestedVersion = detectRequestedPklVersion(modulePath, options);
//noinspection resource
distribution = findCompatibleDistribution(modulePath, requestedVersion, options);
output = distribution.evaluatePath(modulePath, options);
} catch (RuntimeException e) {
exception = e;
// Could log exception, but this would violate "don't log and throw",
// and Pkl stack trace might contain semi-sensitive information.
logFinished(modulePath, false, requestedVersion, distribution, startTime, System.nanoTime());
throw e;
}
var endTime = System.nanoTime();
logFinished(modulePath, true, requestedVersion, distribution, startTime, System.nanoTime());
return output;
}
// Could log exception, but this would violate "don't log and throw",
// and Pkl stack trace might contain semi-sensitive information.
private static void logFinished(
Path modulePath,
boolean success,
@Nullable Version requestedVersion,
@Nullable PklDistribution distribution,
long startTime,
long endTime) {
logger.info(
"Finished evaluating Pkl module. modulePath={} outcome={} requestedVersion={} selectedVersion={} elapsedMillis={}",
modulePath,
exception == null ? "success" : "failure",
success,
requestedVersion == null ? "n/a" : requestedVersion.toString(),
distribution == null ? "n/a" : distribution.getVersion().toString(),
(endTime - startTime) / 1_000_000);
if (exception != null) throw exception;
assert output != null;
return output;
}
private Version detectRequestedPklVersion(Path modulePath, ExecutorOptions options) {
@@ -126,7 +130,7 @@ final class EmbeddedExecutor implements Executor {
toDisplayPath(modulePath, options), availableVersions));
}
/* @Nullable */ static Version extractMinPklVersion(String sourceText) {
static @Nullable Version extractMinPklVersion(String sourceText) {
var matcher = MODULE_INFO_PATTERN.matcher(sourceText);
return matcher.find() ? Version.parse(matcher.group(1)) : null;
}
@@ -177,7 +181,7 @@ final class EmbeddedExecutor implements Executor {
private static final class PklDistribution implements AutoCloseable {
final URLClassLoader pklDistributionClassLoader;
final /* @Nullable */ ExecutorSpi executorSpi;
final ExecutorSpi executorSpi;
final Version version;
/**
@@ -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.
@@ -16,24 +16,26 @@
package org.pkl.executor;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
/**
* Indicates an {@link Executor} error. {@link #getMessage()} returns a user-facing error message.
*/
public final class ExecutorException extends RuntimeException {
private final String pklVersion;
private final @Nullable String pklVersion;
public ExecutorException(String message) {
super(message);
pklVersion = null;
}
public ExecutorException(String message, Throwable cause) {
public ExecutorException(@Nullable String message, @Nullable Throwable cause) {
super(message, cause);
pklVersion = null;
}
public ExecutorException(String message, Throwable cause, String version) {
public ExecutorException(
@Nullable String message, @Nullable Throwable cause, @Nullable String version) {
super(message, cause);
pklVersion = Objects.requireNonNull(version);
}
@@ -43,12 +45,12 @@ public final class ExecutorException extends RuntimeException {
*
* <p>Returns {@code null} if this exception does not originate from an underlying Pkl evaluator.
*/
public String getPklVersion() {
public @Nullable String getPklVersion() {
return pklVersion;
}
@Override
public String getMessage() {
public @Nullable String getMessage() {
var message = super.getMessage();
if (pklVersion == null) {
return message;
@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 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.
@@ -21,6 +21,7 @@ import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
import org.pkl.executor.spi.v1.ExecutorSpiOptions3;
@@ -41,15 +42,15 @@ public final class ExecutorOptions {
private final List<Path> modulePath;
private final /* @Nullable */ Path rootDir;
private final @Nullable Path rootDir;
private final /* @Nullable */ Duration timeout;
private final @Nullable Duration timeout;
private final /* @Nullable */ String outputFormat;
private final @Nullable String outputFormat;
private final /* @Nullable */ Path moduleCacheDir;
private final @Nullable Path moduleCacheDir;
private final /* @Nullable */ Path projectDir;
private final @Nullable Path projectDir;
private final List<Path> certificateFiles;
@@ -81,11 +82,11 @@ public final class ExecutorOptions {
private Map<String, String> environmentVariables = Map.of();
private Map<String, String> externalProperties = Map.of();
private List<Path> modulePath = List.of();
private /* @Nullable */ Path rootDir;
private /* @Nullable */ Duration timeout;
private /* @Nullable */ String outputFormat;
private /* @Nullable */ Path moduleCacheDir;
private /* @Nullable */ Path projectDir;
private @Nullable Path rootDir;
private @Nullable Duration timeout;
private @Nullable String outputFormat;
private @Nullable Path moduleCacheDir;
private @Nullable Path projectDir;
private List<Path> certificateFiles = List.of();
private List<byte[]> certificateBytes = List.of();
private Map<URI, URI> httpRewrites = Map.of();
@@ -270,11 +271,11 @@ public final class ExecutorOptions {
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
/* @Nullable */ Path rootDir,
/* @Nullable */ Duration timeout,
/* @Nullable */ String outputFormat,
/* @Nullable */ Path moduleCacheDir,
/* @Nullable */ Path projectDir) {
@Nullable Path rootDir,
@Nullable Duration timeout,
@Nullable String outputFormat,
@Nullable Path moduleCacheDir,
@Nullable Path projectDir) {
this(
allowedModules,
@@ -300,11 +301,11 @@ public final class ExecutorOptions {
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
/* @Nullable */ Path rootDir,
/* @Nullable */ Duration timeout,
/* @Nullable */ String outputFormat,
/* @Nullable */ Path moduleCacheDir,
/* @Nullable */ Path projectDir,
@Nullable Path rootDir,
@Nullable Duration timeout,
@Nullable String outputFormat,
@Nullable Path moduleCacheDir,
@Nullable Path projectDir,
List<Path> certificateFiles,
List<byte[]> certificateBytes,
Map<URI, URI> httpRewrites,
@@ -354,17 +355,17 @@ public final class ExecutorOptions {
}
/** API equivalent of the {@code --root-dir} CLI option. */
public /* @Nullable */ Path getRootDir() {
public @Nullable Path getRootDir() {
return rootDir;
}
/** API equivalent of the {@code --timeout} CLI option. */
public Duration getTimeout() {
public @Nullable Duration getTimeout() {
return timeout;
}
/** API equivalent of the {@code --format} CLI option. */
public /* @Nullable */ String getOutputFormat() {
public @Nullable String getOutputFormat() {
return outputFormat;
}
@@ -372,7 +373,7 @@ public final class ExecutorOptions {
* API equivalent of the {@code --cache-dir} CLI option. {@code null} is equivalent to {@code
* --no-cache}.
*/
public /* @Nullable */ Path getModuleCacheDir() {
public @Nullable Path getModuleCacheDir() {
return moduleCacheDir;
}
@@ -382,7 +383,7 @@ public final class ExecutorOptions {
* <p>Unlike the CLI, this option only sets project dependencies. It does not set evaluator
* settings.
*/
public /* @Nullable */ Path getProjectDir() {
public @Nullable Path getProjectDir() {
return projectDir;
}
@@ -407,11 +408,10 @@ public final class ExecutorOptions {
}
@Override
public boolean equals(/* @Nullable */ Object obj) {
public boolean equals(@Nullable Object obj) {
if (this == obj) return true;
if (obj.getClass() != ExecutorOptions.class) return false;
if (!(obj instanceof ExecutorOptions other)) return false;
var other = (ExecutorOptions) obj;
return allowedModules.equals(other.allowedModules)
&& allowedResources.equals(other.allowedResources)
&& environmentVariables.equals(other.environmentVariables)
@@ -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.executor;
import java.util.*;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
/**
* A <a href="https://semver.org/spec/v2.0.0.html">semantic version</a>.
@@ -54,19 +55,15 @@ final class Version implements Comparable<Version> {
private final int major;
private final int minor;
private final int patch;
private final /*@Nullable*/ String preRelease;
private final /*@Nullable*/ String build;
private final @Nullable String preRelease;
private final @Nullable String build;
// always access through getter
private volatile Identifier[] __preReleaseIdentifiers;
private volatile Identifier @Nullable [] __preReleaseIdentifiers;
/** Constructs a semantic version. */
public Version(
int major,
int minor,
int patch,
/*@Nullable*/ String preRelease,
/*@Nullable*/ String build) {
int major, int minor, int patch, @Nullable String preRelease, @Nullable String build) {
this.major = major;
this.minor = minor;
this.patch = patch;
@@ -99,7 +96,7 @@ final class Version implements Comparable<Version> {
* <p>Returns {@code null} if the given string could not be parsed as a semantic version number or
* is too large to fit into a {@link Version}.
*/
public static /*@Nullable*/ Version parseOrNull(String version) {
public static @Nullable Version parseOrNull(String version) {
var matcher = VERSION.matcher(version);
if (!matcher.matches()) return null;
@@ -151,22 +148,22 @@ final class Version implements Comparable<Version> {
}
/** Returns the pre-release version (if any). */
public /*@Nullable*/ String getPreRelease() {
public @Nullable String getPreRelease() {
return preRelease;
}
/** Returns a copy of this version with the given pre-release version. */
public Version withPreRelease(/*@Nullable*/ String preRelease) {
public Version withPreRelease(@Nullable String preRelease) {
return new Version(major, minor, patch, preRelease, build);
}
/** Returns the build metadata (if any). */
public /*@Nullable*/ String getBuild() {
public @Nullable String getBuild() {
return build;
}
/** Returns a copy of this version with the given build metadata. */
public Version withBuild(/*@Nullable*/ String build) {
public Version withBuild(@Nullable String build) {
return new Version(major, minor, patch, preRelease, build);
}
@@ -196,7 +193,7 @@ final class Version implements Comparable<Version> {
/** Tells if this version is equal to {@code obj} according to semantic versioning rules. */
@Override
public boolean equals(/* @Nullable */ Object obj) {
public boolean equals(@Nullable Object obj) {
if (this == obj) return true;
if (!(obj instanceof Version other)) return false;
return major == other.major
@@ -222,8 +219,9 @@ final class Version implements Comparable<Version> {
}
private Identifier[] getPreReleaseIdentifiers() {
if (__preReleaseIdentifiers == null) {
__preReleaseIdentifiers =
var result = __preReleaseIdentifiers;
if (result == null) {
result =
preRelease == null
? new Identifier[0]
: Arrays.stream(preRelease.split("\\."))
@@ -233,11 +231,12 @@ final class Version implements Comparable<Version> {
? new Identifier(Long.parseLong(str), null)
: new Identifier(-1, str))
.toArray(Identifier[]::new);
__preReleaseIdentifiers = result;
}
return __preReleaseIdentifiers;
return result;
}
private record Identifier(long numericId, /*@Nullable*/ String alphanumericId)
private record Identifier(long numericId, @Nullable String alphanumericId)
implements Comparable<Identifier> {
@Override
@@ -0,0 +1,4 @@
@NullMarked
package org.pkl.executor;
import org.jspecify.annotations.NullMarked;
@@ -9,4 +9,7 @@
* <li>X MUST only use classes from its own package and Java platform packages.
* </ol>
*/
@NullMarked
package org.pkl.executor.spi;
import org.jspecify.annotations.NullMarked;
@@ -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.
@@ -19,6 +19,7 @@ import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
public class ExecutorSpiOptions {
private final List<String> allowedModules;
@@ -31,13 +32,13 @@ public class ExecutorSpiOptions {
private final List<Path> modulePath;
private final Path rootDir;
private final @Nullable Path rootDir;
private final Duration timeout;
private final @Nullable Duration timeout;
private final String outputFormat;
private final Path moduleCacheDir;
private final Path projectDir;
private final @Nullable String outputFormat;
private final @Nullable Path moduleCacheDir;
private final @Nullable Path projectDir;
public ExecutorSpiOptions(
List<String> allowedModules,
@@ -45,11 +46,11 @@ public class ExecutorSpiOptions {
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
/* @Nullable */ Path rootDir,
/* @Nullable */ Duration timeout,
/* @Nullable */ String outputFormat,
/* @Nullable */ Path moduleCacheDir,
/* @Nullable */ Path projectDir) {
@Nullable Path rootDir,
@Nullable Duration timeout,
@Nullable String outputFormat,
@Nullable Path moduleCacheDir,
@Nullable Path projectDir) {
this.allowedModules = allowedModules;
this.allowedResources = allowedResources;
@@ -83,23 +84,23 @@ public class ExecutorSpiOptions {
return modulePath;
}
public /* @Nullable */ Path getRootDir() {
public @Nullable Path getRootDir() {
return rootDir;
}
public /* @Nullable */ Duration getTimeout() {
public @Nullable Duration getTimeout() {
return timeout;
}
public /* @Nullable */ String getOutputFormat() {
public @Nullable String getOutputFormat() {
return outputFormat;
}
public /* @Nullable */ Path getModuleCacheDir() {
public @Nullable Path getModuleCacheDir() {
return moduleCacheDir;
}
public Path getProjectDir() {
public @Nullable Path getProjectDir() {
return projectDir;
}
}
@@ -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.
@@ -19,6 +19,7 @@ import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
private final List<Path> certificateFiles;
@@ -33,11 +34,11 @@ public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
Path rootDir,
Duration timeout,
String outputFormat,
Path moduleCacheDir,
Path projectDir,
@Nullable Path rootDir,
@Nullable Duration timeout,
@Nullable String outputFormat,
@Nullable Path moduleCacheDir,
@Nullable Path projectDir,
List<Path> certificateFiles,
List<byte[]> certificateBytes,
int testPort) {
@@ -1,5 +1,5 @@
/*
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2025-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.
@@ -20,6 +20,7 @@ import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
public class ExecutorSpiOptions3 extends ExecutorSpiOptions2 {
@@ -31,11 +32,11 @@ public class ExecutorSpiOptions3 extends ExecutorSpiOptions2 {
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
Path rootDir,
Duration timeout,
String outputFormat,
Path moduleCacheDir,
Path projectDir,
@Nullable Path rootDir,
@Nullable Duration timeout,
@Nullable String outputFormat,
@Nullable Path moduleCacheDir,
@Nullable Path projectDir,
List<Path> certificateFiles,
List<byte[]> certificateBytes,
int testPort,
@@ -0,0 +1,4 @@
@NullMarked
package org.pkl.executor.spi.v1;
import org.jspecify.annotations.NullMarked;