From 32d971afa738a7316efe99c74e273e4a8548bf69 Mon Sep 17 00:00:00 2001 From: Alexandru Nedelcu Date: Fri, 1 May 2026 08:14:36 +0300 Subject: [PATCH 1/6] JSpecify fixes --- .../main/java/org/funfix/tasks/jvm/CompletionCallback.java | 7 +++++++ tasks-jvm/src/test/java/org/funfix/tasks/jvm/PureTest.java | 7 +++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java index 5040be3..22655bd 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java @@ -1,6 +1,7 @@ package org.funfix.tasks.jvm; import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.io.Serializable; @@ -22,10 +23,16 @@ * * @param is the type of the value that the task will complete with */ +@NullMarked @FunctionalInterface public interface CompletionCallback extends Serializable { + /** + * Signals the completion of the task. + * + * @param outcome + */ void onOutcome(Outcome outcome); /** diff --git a/tasks-jvm/src/test/java/org/funfix/tasks/jvm/PureTest.java b/tasks-jvm/src/test/java/org/funfix/tasks/jvm/PureTest.java index 931d87e..c606b6a 100644 --- a/tasks-jvm/src/test/java/org/funfix/tasks/jvm/PureTest.java +++ b/tasks-jvm/src/test/java/org/funfix/tasks/jvm/PureTest.java @@ -1,13 +1,12 @@ package org.funfix.tasks.jvm; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -import java.io.IOException; import java.util.concurrent.ExecutionException; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; public class PureTest { + @Test void pureTask() throws ExecutionException, InterruptedException { final var task = Task.pure(42); From cd2133cfa3fde040f1b8719d8ffffda5bf476d02 Mon Sep 17 00:00:00 2001 From: Alexandru Nedelcu Date: Fri, 1 May 2026 08:21:35 +0300 Subject: [PATCH 2/6] Add skill --- .agents/skills/jspecify-nullness/SKILL.md | 35 ++++ .../references/jspecify-nullness.md | 160 ++++++++++++++++++ .claude/skills/jspecify-nullness | 1 + skills-lock.json | 11 ++ 4 files changed, 207 insertions(+) create mode 100644 .agents/skills/jspecify-nullness/SKILL.md create mode 100644 .agents/skills/jspecify-nullness/references/jspecify-nullness.md create mode 120000 .claude/skills/jspecify-nullness create mode 100644 skills-lock.json diff --git a/.agents/skills/jspecify-nullness/SKILL.md b/.agents/skills/jspecify-nullness/SKILL.md new file mode 100644 index 0000000..ebc82e9 --- /dev/null +++ b/.agents/skills/jspecify-nullness/SKILL.md @@ -0,0 +1,35 @@ +--- +name: jspecify-nullness +description: JSpecify nullness annotations for Java APIs and tooling. Use when adopting or migrating JSpecify annotations, designing null-safe Java signatures and generics, or interpreting tool conformance and Kotlin interop. +--- + +# JSpecify Nullness (Java) + +## Quick start +- Use `@NullMarked` to make unannotated types non-null by default. +- Add `@Nullable` only where null is allowed; use `@NonNull` sparingly when you must override a nullable type variable use. +- Decide type parameter bounds up front: `` allows nullable type arguments, `` does not. +- Use `@NullUnmarked` to opt out of `@NullMarked` in legacy or incremental areas. +- Keep annotations in recognized type-use positions (arrays, nested types, type arguments). +- Read `references/jspecify-nullness.md` for migration steps, syntax pitfalls, and tooling details. + +## Workflow +1. Confirm tool support and constraints (nullness checker, Kotlin version, annotation processors). +2. Add the `org.jspecify:jspecify` dependency and expose it to consumers. +3. Annotate nullable types first, then add `@NullMarked` at class or package scope. +4. Fix generics: set bounds for type parameters and annotate type-variable uses as needed. +5. Run nullness analysis, resolve findings, and repeat for adjacent code. + +## Rules of thumb +- Treat unannotated types outside `@NullMarked` as unspecified nullness, not non-null. +- Do not annotate local-variable root types; annotate only type arguments or array components there. +- For arrays, `@Nullable String[]` means nullable elements, while `String @Nullable []` means a nullable array. +- For nested types, use `Map.@Nullable Entry` to mark the nested type, not the outer type. +- Use `@Nullable` on a type variable usage only when null is allowed even if the type argument is non-null. + +## Output expectations +- Provide annotated signatures and call-site implications. +- Call out tool-conformance limits rather than promising specific diagnostics. + +## References +- Load `references/jspecify-nullness.md` for full guidance and examples. diff --git a/.agents/skills/jspecify-nullness/references/jspecify-nullness.md b/.agents/skills/jspecify-nullness/references/jspecify-nullness.md new file mode 100644 index 0000000..b650ba8 --- /dev/null +++ b/.agents/skills/jspecify-nullness/references/jspecify-nullness.md @@ -0,0 +1,160 @@ +# JSpecify Nullness - Reference + +Concise guidance for applying JSpecify annotations in Java, including scope defaults, generic type parameters, and tooling constraints. + +Sources: +- https://jspecify.dev/docs/start-here/ +- https://jspecify.dev/docs/user-guide/ +- https://jspecify.dev/docs/whether/ +- https://jspecify.dev/docs/using/ +- https://jspecify.dev/docs/applying/ +- https://jspecify.dev/docs/spec/ +- https://jspecify.dev/docs/tool-conformance/ + + +## Table of Contents +- [Goals and model](#goals-and-model) +- [Core annotations](#core-annotations) +- [Null-marked scopes](#null-marked-scopes) +- [Type-use placement rules](#type-use-placement-rules) +- [Generics and type parameters](#generics-and-type-parameters) +- [Adoption and migration](#adoption-and-migration) +- [Tooling and conformance](#tooling-and-conformance) +- [Kotlin and annotation processors](#kotlin-and-annotation-processors) +- [Checklist](#checklist) +- [Test prompts](#test-prompts) + +## Goals and model +- JSpecify defines standard nullness annotations and precise semantics for Java type usages. +- The model has four nullness states: nullable, non-null, parametric (type variable), and unspecified. +- `@NullMarked` makes unannotated types non-null by default within the scope; outside it, unannotated types are unspecified. + +## Core annotations +- `@Nullable` means the annotated type usage can include `null`. +- `@NonNull` means the annotated type usage excludes `null`. +- `@NullMarked` applies to modules, packages, classes, or methods and makes unannotated types non-null by default. +- `@NullUnmarked` cancels a surrounding `@NullMarked` for incremental adoption. + +Example: + +```java +@NullMarked +final class Strings { + static @Nullable String emptyToNull(String x) { + return x.isEmpty() ? null : x; + } + + static String nullToEmpty(@Nullable String x) { + return x == null ? "" : x; + } +} +``` + +## Null-marked scopes +- Packages are not hierarchical. `@NullMarked` on `com.example` does not affect `com.example.sub`. +- In a null-marked scope, unannotated types are treated as non-null, but local-variable root types are not annotated. +- `@NullUnmarked` is useful when only part of a package or class is ready for annotations. + +## Type-use placement rules +JSpecify annotations are type-use annotations, so placement matters: + +- Nested types: mark the nested type, not the outer type. + +```java +Map.@Nullable Entry entry; +``` + +- Arrays: the token after the annotation is what can be null. + +```java +@Nullable String[] nullableElements; +String @Nullable [] nullableArray; +@Nullable String @Nullable [] bothNullable; +``` + +- Local variables: do not annotate the root type, but do annotate type arguments or array components. + +```java +List<@Nullable String> names = new ArrayList<>(); +``` + +If a tool reports an unrecognized annotation location, move the annotation to the nearest simple type or component. + +## Generics and type parameters +- A type parameter without an explicit bound behaves like ``. +- Inside `@NullMarked`, that means the type argument is non-null unless you make the bound nullable. + +Allow nullable type arguments: + +```java +@NullMarked +interface Box { + T get(); + void set(T value); +} +``` + +Disallow nullable type arguments: + +```java +@NullMarked +interface ImmutableBox { + T get(); +} +``` + +Use `@Nullable` on a type-variable usage only when the result can be null even when the type argument is non-null: + +```java +@NullMarked +interface ListLike { + @Nullable E firstOrNull(); + E get(int index); +} +``` + +Use `@NonNull` on a type-variable usage to force non-null even when the type argument is nullable: + +```java +@NullMarked +interface OptionalLike { + Optional<@NonNull T> toOptional(); +} +``` + +## Adoption and migration +Recommended steps for a new codebase or incremental migration: + +1. Add the dependency `org.jspecify:jspecify:1.0.0` and do not hide it from consumers. +2. Start with a small, low-dependency package or class. +3. Mark obvious nullable usages (`return null`, `if (x == null)`, nullable fields). +4. Add `@NullMarked` to the scope and use `@NullUnmarked` for unannotated areas. +5. Fix type parameters and usages in generics. +6. Run nullness analysis and address findings, then expand to calling code. + +Migration from JSR-305 or other annotations: +- Update imports to JSpecify. +- Fix array and nested-type placements to comply with type-use rules. +- Expect some build errors due to type-use placement restrictions. + +## Tooling and conformance +- JSpecify defines semantics, not required diagnostics. Tools may choose their own error or warning behavior. +- Conformance is described in terms of questions about whether a usage is recognized, what the augmented type is, and whether an expression is a nullness subtype of its context. +- Some tools are partially conformant, especially around generics or unspecified nullness. + +## Kotlin and annotation processors +- Kotlin compilers recognize JSpecify annotations; support level varies by version for `@Nullable`, `@NonNull`, and `@NullUnmarked`. +- If you rely on annotation processors like Dagger, you may need JDK 22+ due to older `javac` type-use annotation bugs. + +## Checklist +- Use `@NullMarked` at package or class scope. +- Mark nullable return types and parameters with `@Nullable`. +- Decide per type parameter whether nullable type arguments are allowed. +- Verify type-use placement for arrays and nested types. +- Run a JSpecify-aware nullness checker and resolve findings. + +## Test prompts +- "Annotate this API with JSpecify, including generics and `@NullMarked`, and explain call-site impacts." +- "Migrate these JSR-305 annotations to JSpecify and fix any type-use placement issues." +- "Given this signature, decide if the type parameter bound should be nullable and justify it." +- "Explain why `Map.@Nullable Entry` is required here and how arrays should be annotated." diff --git a/.claude/skills/jspecify-nullness b/.claude/skills/jspecify-nullness new file mode 120000 index 0000000..b59666f --- /dev/null +++ b/.claude/skills/jspecify-nullness @@ -0,0 +1 @@ +../../.agents/skills/jspecify-nullness \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..e043180 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "jspecify-nullness": { + "source": "alexandru/skills", + "sourceType": "github", + "skillPath": "skills/jspecify-nullness/SKILL.md", + "computedHash": "7cd2c85c83bdec95a36ea222db3dce9afc83f8b82b0c3e05602982e364095b46" + } + } +} From b4017ff7da1777d426fe157cbb4fc3efb965451e Mon Sep 17 00:00:00 2001 From: Alexandru Nedelcu Date: Fri, 1 May 2026 10:07:01 +0300 Subject: [PATCH 3/6] fix(jvm): correct jspecify nullness annotations - Add NullAway/Error Prone verification for tasks-jvm main Java compilation - Standardize nullable annotations to org.jspecify.annotations.Nullable - Correct generic bounds for nullable type arguments (extends @Nullable Object) - Fix type-use annotations for void/sentinel task types (@Nullable Void) - Document NullAway suppressions with justifications - Preserve all runtime behavior and existing contracts - Kotlin interop verified: no Kotlin source changes needed - All Gradle checks pass: ./gradlew check --no-daemon --- buildSrc/build.gradle.kts | 3 ++- gradle/libs.versions.toml | 8 +++++++ tasks-jvm/build.gradle.kts | 24 +++++++++++++++++++ .../org/funfix/tasks/jvm/Cancellable.java | 9 ++++--- .../funfix/tasks/jvm/CancellableFuture.java | 2 +- .../org/funfix/tasks/jvm/CloseableFun.java | 9 ++++--- .../org/funfix/tasks/jvm/Collections.java | 7 +++--- .../funfix/tasks/jvm/CompletionCallback.java | 6 ++++- .../main/java/org/funfix/tasks/jvm/Fiber.java | 4 ++-- .../java/org/funfix/tasks/jvm/Resource.java | 7 +++--- .../main/java/org/funfix/tasks/jvm/Task.java | 5 ++-- .../org/funfix/tasks/jvm/TaskExecutor.java | 2 +- 12 files changed, 63 insertions(+), 23 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index f96debf..4854031 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -23,7 +23,8 @@ fun version(k: String) = dependencies { implementation(libs.gradle.versions.plugin) implementation(libs.vanniktech.publish.plugin) - // removed errorprone plugin + implementation(libs.errorprone.gradle.plugin) + implementation(libs.nullaway.gradle.plugin) // Provide plugins used by precompiled script plugins so their ids are available implementation(libs.kotlin.gradle.plugin) implementation(libs.kover.gradle.plugin) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe0456f..7cb9496 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,10 +2,14 @@ arrow = "2.2.1.1" binary-compatibility-validator = "0.18.1" dokka = "2.1.0" +errorprone = "2.42.0" +errorprone-plugin = "5.1.0" jetbrains-annotations = "26.0.2" jspecify = "1.0.0" kotlin = "2.3.0" kover = "0.9.5" +nullaway = "0.13.1" +nullaway-plugin = "3.0.0" publish-plugin = "0.36.0" versions-plugin = "0.53.0" kotlinx-coroutines = "1.10.2" @@ -14,14 +18,18 @@ kotlinx-coroutines = "1.10.2" # Plugins specified in buildSrc/build.gradle.kts binary-compatibility-validator-plugin = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version.ref = "binary-compatibility-validator" } dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } +errorprone-gradle-plugin = { module = "net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin", version.ref = "errorprone-plugin" } gradle-versions-plugin = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "versions-plugin" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kover-gradle-plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } +nullaway-gradle-plugin = { module = "net.ltgt.nullaway:net.ltgt.nullaway.gradle.plugin", version.ref = "nullaway-plugin" } vanniktech-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish-plugin" } # Actual libraries +errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" } +nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } diff --git a/tasks-jvm/build.gradle.kts b/tasks-jvm/build.gradle.kts index 1df7cb5..ff37b36 100644 --- a/tasks-jvm/build.gradle.kts +++ b/tasks-jvm/build.gradle.kts @@ -1,6 +1,12 @@ +import net.ltgt.gradle.errorprone.CheckSeverity +import net.ltgt.gradle.errorprone.errorprone +import net.ltgt.gradle.nullaway.nullaway + plugins { id("tasks.java-project") id("tasks.versions") + id("net.ltgt.errorprone") + id("net.ltgt.nullaway") } mavenPublishing { @@ -14,6 +20,8 @@ dependencies { api(libs.jspecify) compileOnly(libs.jetbrains.annotations) + errorprone(libs.errorprone.core) + errorprone(libs.nullaway) testImplementation(platform("org.junit:junit-bom:6.0.2")) testImplementation("org.junit.jupiter:junit-jupiter") @@ -39,6 +47,22 @@ tasks.withType { } +tasks.named("compileJava") { + options.errorprone { + check("RequireExplicitNullMarking", CheckSeverity.ERROR) + nullaway { + error() + onlyNullMarked = true + } + } +} + +tasks.named("compileTestJava") { + options.errorprone.nullaway { + disable() + } +} + tasks.register("testsOn21") { useJUnitPlatform() javaLauncher = javaToolchains.launcherFor { diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Cancellable.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Cancellable.java index b072603..5a3c9eb 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Cancellable.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Cancellable.java @@ -2,7 +2,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NonBlocking; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.Nullable; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; @@ -86,7 +86,6 @@ final class MutableCancellable implements Cancellable { @Override public void cancel() { - @Nullable var state = ref.getAndSet(State.Closed.INSTANCE); while (state instanceof State.Active active) { try { @@ -151,8 +150,8 @@ private void unregister(final long order) { while (true) { final var current = ref.get(); if (current instanceof State.Active active) { - @Nullable var cursor = active; - @Nullable State.Active acc = null; + State.@Nullable Active cursor = active; + State.@Nullable Active acc = null; while (cursor != null) { if (cursor.order != order) { acc = new State.Active(cursor.token, cursor.order, acc); @@ -160,7 +159,7 @@ private void unregister(final long order) { cursor = cursor.rest; } // Reversing - @Nullable State.Active update = null; + State.@Nullable Active update = null; while (acc != null) { update = new State.Active(acc.token, acc.order, update); acc = acc.rest; diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CancellableFuture.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CancellableFuture.java index 6abcafa..c2e8895 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CancellableFuture.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CancellableFuture.java @@ -22,7 +22,7 @@ public record CancellableFuture( CompletableFuture future, Cancellable cancellable ) { - public CancellableFuture transform( + public CancellableFuture transform( Function, ? extends CompletableFuture> fn ) { return new CancellableFuture<>(fn.apply(future), cancellable); diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CloseableFun.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CloseableFun.java index 25f338b..fc6d88c 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CloseableFun.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CloseableFun.java @@ -1,5 +1,7 @@ package org.funfix.tasks.jvm; +import org.jspecify.annotations.Nullable; + import java.util.function.Function; /** @@ -15,10 +17,11 @@ public interface CloseableFun extends AutoCloseable { * Converts this blocking finalizer into an asynchronous one * that can be used for initializing {@link Resource.Acquired}. */ - default Function> toAsync() { + default Function> toAsync() { + // NullAway does not propagate the @Nullable Void type witness into the lambda body. @SuppressWarnings("NullAway") - final Function> r = - exitCase -> TaskUtils.taskUninterruptibleBlockingIO(() -> { + final Function> r = + exitCase -> TaskUtils.<@Nullable Void>taskUninterruptibleBlockingIO(() -> { close(exitCase); return null; }); diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Collections.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Collections.java index 608e9c6..31d578c 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Collections.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Collections.java @@ -73,7 +73,7 @@ public int hashCode() { } } - static final class Nil extends ImmutableStack { + static final class Nil extends ImmutableStack { Nil() {} @Override @@ -87,7 +87,7 @@ public boolean equals(Object obj) { } } - static ImmutableStack empty() { + static ImmutableStack empty() { return new Nil<>(); } @@ -158,6 +158,7 @@ ImmutableQueue preOptimize() { } } + // NullAway cannot refine ImmutableStack.head() after the explicit non-empty checks. @SuppressWarnings("NullAway") T peek() throws NoSuchElementException { if (!toDequeue.isEmpty()) { @@ -248,7 +249,7 @@ public int hashCode() { return toList().hashCode(); } - static ImmutableQueue empty() { + static ImmutableQueue empty() { return new ImmutableQueue<>(ImmutableStack.empty(), ImmutableStack.empty()); } } diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java index 22655bd..3f05e8a 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java @@ -183,6 +183,7 @@ public AsyncContinuationCallback( } @Override + // NullAway does not connect the nullable state fields with the completion branch that populated them. @SuppressWarnings("NullAway") public void run() { if (this.outcome != null) { @@ -239,7 +240,7 @@ public void onCancellation() { } } - public static ContinuationCallback protect( + public static ContinuationCallback protect( final TaskExecutor executor, final CompletionCallback listener ) { @@ -250,6 +251,7 @@ public static ContinuationCallback protect( ); } + // NullAway treats AtomicReference.get() as nullable between the read and compare-and-set. @SuppressWarnings("NullAway") public void registerExtraCallback(CompletionCallback extraCallback) { while (true) { @@ -289,6 +291,7 @@ final class BlockingCompletionCallback @Nullable private InterruptedException interrupted = null; + // NullAway cannot prove that a successful completion stored the nullable-bounded T result. @SuppressWarnings("NullAway") private void notifyOutcome() { final var extraCallback = extraCallbackRef.getAndSet(null); @@ -360,6 +363,7 @@ interface AwaitFunction { void apply(boolean isCancelled) throws InterruptedException, TimeoutException; } + // NullAway cannot prove await completion initialized nullable-bounded T before returning. @SuppressWarnings("NullAway") private T awaitInline(final Cancellable cancelToken, final AwaitFunction await) throws InterruptedException, ExecutionException, TimeoutException { diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Fiber.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Fiber.java index 42445a5..86c22a0 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Fiber.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Fiber.java @@ -243,8 +243,8 @@ default T awaitBlocking() throws InterruptedException, TaskCancellationException * that you need to call {@link Fiber#cancel()}. */ @NonBlocking - default CancellableFuture joinAsync() { - final var future = new CompletableFuture(); + default CancellableFuture<@Nullable Void> joinAsync() { + final var future = new CompletableFuture<@Nullable Void>(); @SuppressWarnings("DataFlowIssue") final var token = joinAsync(() -> future.complete(null)); final Cancellable cRef = () -> { diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Resource.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Resource.java index 868004c..1a9ad55 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Resource.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Resource.java @@ -280,7 +280,7 @@ public static Resource fromAutoCloseable( */ public record Acquired( T get, - Function> releaseTask + Function> releaseTask ) implements AutoCloseable { /** * Used for asynchronous resource release. @@ -288,7 +288,7 @@ public record Acquired( * @param exitCase signals the context in which the resource is being released. * @return a {@link Task} that releases the resource upon invocation. */ - public Task releaseTask(final ExitCase exitCase) { + public Task<@Nullable Void> releaseTask(final ExitCase exitCase) { return releaseTask.apply(exitCase); } @@ -354,12 +354,11 @@ public void close() throws Exception { ) { Objects.requireNonNull(resource, "resource"); Objects.requireNonNull(release, "release"); - @SuppressWarnings("NullAway") final var acquired = new Acquired<>(resource, release.toAsync()); return acquired; } - private static final Function> NOOP = + private static final Function> NOOP = ignored -> Task.NOOP; } } diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Task.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Task.java index ff0acc7..8259e1f 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Task.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/Task.java @@ -474,8 +474,9 @@ public T runBlockingTimed(final Duration timeout) } /** Reusable "void" task that does nothing, completing immediately. */ + // NullAway does not accept the null sentinel even with the explicit @Nullable Void type witness. @SuppressWarnings({"NullAway", "DataFlowIssue"}) - public static final Task NOOP = Task.pure(null); + public static final Task<@Nullable Void> NOOP = Task.<@Nullable Void>pure(null); } /** @@ -520,7 +521,7 @@ public void invoke(Continuation continuation) { } } - private static CompletableFuture getCompletableFuture( + private static CompletableFuture getCompletableFuture( CancellableFuture cancellableFuture, CompletionCallback callback ) { diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/TaskExecutor.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/TaskExecutor.java index 946c133..62e06a1 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/TaskExecutor.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/TaskExecutor.java @@ -1,7 +1,7 @@ package org.funfix.tasks.jvm; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.Nullable; import java.util.concurrent.Executor; From bd2de98bec56be4b485e37eeee3462481896c8d8 Mon Sep 17 00:00:00 2001 From: Alexandru Nedelcu Date: Fri, 1 May 2026 11:26:31 +0300 Subject: [PATCH 4/6] Revert change on CompletionCallback --- .../funfix/tasks/jvm/CompletionCallback.java | 110 ++++++++++-------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java index 3f05e8a..4cd0e08 100644 --- a/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java +++ b/tasks-jvm/src/main/java/org/funfix/tasks/jvm/CompletionCallback.java @@ -1,9 +1,5 @@ package org.funfix.tasks.jvm; -import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - import java.io.Serializable; import java.time.Duration; import java.util.Objects; @@ -12,6 +8,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.AbstractQueuedSynchronizer; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Represents a callback that will be invoked when a task completes. @@ -23,11 +22,10 @@ * * @param is the type of the value that the task will complete with */ -@NullMarked @FunctionalInterface -public interface CompletionCallback - extends Serializable { - +public interface CompletionCallback< + T extends @Nullable Object +> extends Serializable { /** * Signals the completion of the task. * @@ -73,8 +71,9 @@ default void onCancellation() { } @ApiStatus.Internal -final class ManyCompletionCallback - implements CompletionCallback { +final class ManyCompletionCallback< + T extends @Nullable Object +> implements CompletionCallback { private final ImmutableStack> listeners; @@ -96,7 +95,9 @@ private ManyCompletionCallback( this.listeners = listeners; } - ManyCompletionCallback withExtraListener(CompletionCallback extraListener) { + ManyCompletionCallback withExtraListener( + CompletionCallback extraListener + ) { Objects.requireNonNull(extraListener, "extraListener"); final var newListeners = this.listeners.prepend(extraListener); return new ManyCompletionCallback<>(newListeners); @@ -149,9 +150,9 @@ public void onCancellation() { } @ApiStatus.Internal -interface ContinuationCallback - extends CompletionCallback, Serializable { - +interface ContinuationCallback< + T extends @Nullable Object +> extends CompletionCallback, Serializable { /** * Registers an extra callback to be invoked when the task completes. * This is useful for chaining callbacks or adding additional listeners. @@ -160,8 +161,9 @@ interface ContinuationCallback } @ApiStatus.Internal -final class AsyncContinuationCallback - implements ContinuationCallback, Runnable { +final class AsyncContinuationCallback< + T extends @Nullable Object +> implements ContinuationCallback, Runnable { private final AtomicBoolean isWaiting = new AtomicBoolean(true); private final AtomicReference> listenerRef; @@ -245,10 +247,7 @@ public void onCancellation() { final CompletionCallback listener ) { Objects.requireNonNull(listener, "listener"); - return new AsyncContinuationCallback<>( - listener, - executor - ); + return new AsyncContinuationCallback<>(listener, executor); } // NullAway treats AtomicReference.get() as nullable between the read and compare-and-set. @@ -261,7 +260,12 @@ public void registerExtraCallback(CompletionCallback extraCallback) { if (listenerRef.compareAndSet(current, update)) { return; } - } else if (listenerRef.compareAndSet(current, new ManyCompletionCallback<>(current, extraCallback))) { + } else if ( + listenerRef.compareAndSet( + current, + new ManyCompletionCallback<>(current, extraCallback) + ) + ) { return; } } @@ -277,17 +281,21 @@ public void registerExtraCallback(CompletionCallback extraCallback) { */ @ApiStatus.Internal final class BlockingCompletionCallback - extends AbstractQueuedSynchronizer implements ContinuationCallback { + extends AbstractQueuedSynchronizer + implements ContinuationCallback +{ - private final AtomicBoolean isDone = - new AtomicBoolean(false); - private final AtomicReference<@Nullable CompletionCallback> extraCallbackRef = - new AtomicReference<>(null); + private final AtomicBoolean isDone = new AtomicBoolean(false); + private final AtomicReference< + @Nullable CompletionCallback + > extraCallbackRef = new AtomicReference<>(null); @Nullable private T result = null; + @Nullable private Throwable error = null; + @Nullable private InterruptedException interrupted = null; @@ -295,17 +303,13 @@ final class BlockingCompletionCallback @SuppressWarnings("NullAway") private void notifyOutcome() { final var extraCallback = extraCallbackRef.getAndSet(null); - if (extraCallback != null) - try { - if (error != null) - extraCallback.onFailure(error); - else if (interrupted != null) - extraCallback.onCancellation(); - else - extraCallback.onSuccess(result); - } catch (Throwable e) { - UncaughtExceptionHandler.logOrRethrow(e); - } + if (extraCallback != null) try { + if (error != null) extraCallback.onFailure(error); + else if (interrupted != null) extraCallback.onCancellation(); + else extraCallback.onSuccess(result); + } catch (Throwable e) { + UncaughtExceptionHandler.logOrRethrow(e); + } releaseShared(1); } @@ -360,14 +364,16 @@ protected boolean tryReleaseShared(final int arg) { @FunctionalInterface interface AwaitFunction { - void apply(boolean isCancelled) throws InterruptedException, TimeoutException; + void apply(boolean isCancelled) + throws InterruptedException, TimeoutException; } // NullAway cannot prove await completion initialized nullable-bounded T before returning. @SuppressWarnings("NullAway") - private T awaitInline(final Cancellable cancelToken, final AwaitFunction await) - throws InterruptedException, ExecutionException, TimeoutException { - + private T awaitInline( + final Cancellable cancelToken, + final AwaitFunction await + ) throws InterruptedException, ExecutionException, TimeoutException { TaskLocalContext.signalTheStartOfBlockingCall(); var isCancelled = false; TimeoutException timedOut = null; @@ -378,8 +384,7 @@ private T awaitInline(final Cancellable cancelToken, final AwaitFunction await) } catch (final TimeoutException | InterruptedException e) { if (!isCancelled) { isCancelled = true; - if (e instanceof TimeoutException te) - timedOut = te; + if (e instanceof TimeoutException te) timedOut = te; cancelToken.cancel(); } } @@ -396,9 +401,12 @@ private T awaitInline(final Cancellable cancelToken, final AwaitFunction await) return result; } - public T await(final Cancellable cancelToken) throws InterruptedException, ExecutionException { + public T await(final Cancellable cancelToken) + throws InterruptedException, ExecutionException { try { - return awaitInline(cancelToken, isCancelled -> acquireSharedInterruptibly(1)); + return awaitInline(cancelToken, isCancelled -> + acquireSharedInterruptibly(1) + ); } catch (final TimeoutException e) { throw new IllegalStateException("Unexpected timeout", e); } @@ -406,11 +414,12 @@ public T await(final Cancellable cancelToken) throws InterruptedException, Execu public T await(final Cancellable cancelToken, final Duration timeout) throws ExecutionException, InterruptedException, TimeoutException { - return awaitInline(cancelToken, isCancelled -> { if (!isCancelled) { if (!tryAcquireSharedNanos(1, timeout.toNanos())) { - throw new TimeoutException("Task timed-out after " + timeout); + throw new TimeoutException( + "Task timed-out after " + timeout + ); } } else { // Waiting without a timeout, since at this point it's waiting @@ -433,7 +442,12 @@ public void registerExtraCallback(CompletionCallback extraCallback) { if (extraCallbackRef.compareAndSet(current, update)) { return; } - } else if (extraCallbackRef.compareAndSet(current, new ManyCompletionCallback<>(current, extraCallback))) { + } else if ( + extraCallbackRef.compareAndSet( + current, + new ManyCompletionCallback<>(current, extraCallback) + ) + ) { return; } } From 762822e02eb004867914e752c1e764b948c80d06 Mon Sep 17 00:00:00 2001 From: Alexandru Nedelcu Date: Fri, 1 May 2026 11:52:31 +0300 Subject: [PATCH 5/6] Disable other warnings --- tasks-jvm/build.gradle.kts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tasks-jvm/build.gradle.kts b/tasks-jvm/build.gradle.kts index ff37b36..2d81dde 100644 --- a/tasks-jvm/build.gradle.kts +++ b/tasks-jvm/build.gradle.kts @@ -44,12 +44,12 @@ tasks.withType { "-Xlint:deprecation", // "-Werror" )) - } tasks.named("compileJava") { options.errorprone { - check("RequireExplicitNullMarking", CheckSeverity.ERROR) + disableAllChecks = true + check("NullAway", CheckSeverity.ERROR) nullaway { error() onlyNullMarked = true @@ -58,8 +58,11 @@ tasks.named("compileJava") { } tasks.named("compileTestJava") { - options.errorprone.nullaway { - disable() + options.errorprone { + disableAllChecks = true + nullaway { + disable() + } } } From 732eb065d813638273c763ee4d3d795ce6130f7e Mon Sep 17 00:00:00 2001 From: Alexandru Nedelcu Date: Fri, 1 May 2026 11:54:44 +0300 Subject: [PATCH 6/6] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 541cfbb..e7217cf 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ project/local-plugins.sbt .sbt-scripted/ local.sbt +# OpenCode +.sisyphus + # Bloop .bsp