Skip to content

Error Prone / NullAway support for JSpecify #196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import net.ltgt.gradle.errorprone.CheckSeverity
import java.text.SimpleDateFormat

plugins {
Expand All @@ -11,11 +14,28 @@ plugins {
id 'io.github.gradle-nexus.publish-plugin' version '1.0.0'
id 'com.github.ben-manes.versions' version '0.51.0'
id "me.champeau.jmh" version "0.7.3"
id "net.ltgt.errorprone" version '4.2.0'

// Kotlin just for tests - not production code
id 'org.jetbrains.kotlin.jvm' version '2.1.21'
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
languageVersion = JavaLanguageVersion.of(17)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to run on 17 to get errorprone happy (compiled for 17)

but we target 11 later

}
}

kotlin {
compilerOptions {
apiVersion = KotlinVersion.KOTLIN_2_0
languageVersion = KotlinVersion.KOTLIN_2_0
jvmTarget = JvmTarget.JVM_11
javaParameters = true
freeCompilerArgs = [
'-Xemit-jvm-type-annotations',
'-Xjspecify-annotations=strict',
]
}
}

Expand Down Expand Up @@ -75,8 +95,35 @@ dependencies {
// this is needed for the idea jmh plugin to work correctly
jmh 'org.openjdk.jmh:jmh-core:1.37'
jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37'

errorprone 'com.uber.nullaway:nullaway:0.12.6'
errorprone 'com.google.errorprone:error_prone_core:2.37.0'

// just tests
testCompileOnly 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
}

tasks.withType(JavaCompile) {
options.release = 11
options.errorprone {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

builds with v17 - targets v11

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm .. I don't think we can do that: build with 17 would allow to use APIs which are in 17, but not in 11, which would result in errors when run with 11.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to achieve errorprone / nullaway checking without this

I took this from @bclozel examples : bclozel@f449972

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be safe as the "-release" option is tailored for that:

When using the --release option, only the supported documented API for that release may be used; you cannot use any options to break encapsulation to access any internal classes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.gradle.org/current/userguide/building_java_projects.html#sec:compiling_with_release

Using release property is possible starting from Java 10.
Selecting a Java release makes sure that compilation is done with the configured language level and against the JDK APIs from that Java version.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are running the v17 compiler but using the "release" of JDK 11 including libraries

disableAllChecks = true
check("NullAway", CheckSeverity.ERROR)
//
// end state has us with this config turned on - eg all classes
//
//option("NullAway:AnnotatedPackages", "org.dataloader")
option("NullAway:OnlyNullMarked", "true")
option("NullAway:JSpecifyMode", "true")
}
// Include to disable NullAway on test code
if (name.toLowerCase().contains("test")) {
options.errorprone {
disable("NullAway")
}
}
}


task sourcesJar(type: Jar) {
dependsOn classes
archiveClassifier.set('sources')
Expand Down
7 changes: 3 additions & 4 deletions src/main/java/org/dataloader/BatchLoaderEnvironment.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
@NullMarked
public class BatchLoaderEnvironment {

private final Object context;
private final @Nullable Object context;
private final Map<Object, Object> keyContexts;
private final List<Object> keyContextsList;

private BatchLoaderEnvironment(Object context, List<Object> keyContextsList, Map<Object, Object> keyContexts) {
private BatchLoaderEnvironment(@Nullable Object context, List<Object> keyContextsList, Map<Object, Object> keyContexts) {
this.context = context;
this.keyContexts = keyContexts;
this.keyContextsList = keyContextsList;
Expand All @@ -33,7 +33,6 @@ private BatchLoaderEnvironment(Object context, List<Object> keyContextsList, Map
* Returns the overall context object provided by {@link org.dataloader.BatchLoaderContextProvider}
*
* @param <T> the type you would like the object to be
*
* @return a context object or null if there isn't one
*/
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -68,7 +67,7 @@ public static Builder newBatchLoaderEnvironment() {
}

public static class Builder {
private Object context;
private @Nullable Object context;
private Map<Object, Object> keyContexts = Collections.emptyMap();
private List<Object> keyContextsList = Collections.emptyList();

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/dataloader/DataLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
*/
@PublicApi
@NullMarked
public class DataLoader<K, V> {
public class DataLoader<K, V extends @Nullable Object> {

private final @Nullable String name;
private final DataLoaderHelper<K, V> helper;
Expand Down
11 changes: 6 additions & 5 deletions src/main/java/org/dataloader/DataLoaderRegistry.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.dataloader;

import org.dataloader.annotations.PublicApi;
import org.dataloader.impl.Assertions;
import org.dataloader.instrumentation.ChainedDataLoaderInstrumentation;
import org.dataloader.instrumentation.DataLoaderInstrumentation;
import org.dataloader.instrumentation.DataLoaderInstrumentationHelper;
Expand All @@ -14,6 +15,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
Expand Down Expand Up @@ -141,8 +143,7 @@ private static DataLoaderOptions setInInstrumentation(DataLoaderOptions options,
* @return this registry
*/
public DataLoaderRegistry register(DataLoader<?, ?> dataLoader) {
String name = dataLoader.getName();
assertState(name != null, () -> "The DataLoader must have a non null name");
String name = Assertions.nonNull(dataLoader.getName(), () -> "The DataLoader must have a non null name");
dataLoaders.put(name, nameAndInstrumentDL(name, instrumentation, dataLoader));
return this;
}
Expand Down Expand Up @@ -176,7 +177,7 @@ public DataLoaderRegistry register(String key, DataLoader<?, ?> dataLoader) {
*/
public <K, V> DataLoader<K, V> registerAndGet(String key, DataLoader<?, ?> dataLoader) {
dataLoaders.put(key, nameAndInstrumentDL(key, instrumentation, dataLoader));
return getDataLoader(key);
return Objects.requireNonNull(getDataLoader(key));
}

/**
Expand Down Expand Up @@ -251,10 +252,10 @@ public DataLoaderRegistry unregister(String key) {
* @param key the key of the data loader
* @param <K> the type of keys
* @param <V> the type of values
* @return a data loader or null if its not present
* @return a data loader or null if it's not present
*/
@SuppressWarnings("unchecked")
public <K, V> DataLoader<K, V> getDataLoader(String key) {
public <K, V> @Nullable DataLoader<K, V> getDataLoader(String key) {
return (DataLoader<K, V>) dataLoaders.get(key);
}

Expand Down
3 changes: 1 addition & 2 deletions src/test/java/org/dataloader/DataLoaderRegistryTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.dataloader;

import org.dataloader.impl.DataLoaderAssertionException;
import org.dataloader.stats.SimpleStatisticsCollector;
import org.dataloader.stats.Statistics;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -63,7 +62,7 @@ public void registration_works() {
try {
registry.register(dlUnnamed);
Assertions.fail("Should have thrown an exception");
} catch (DataLoaderAssertionException ignored) {
} catch (NullPointerException ignored) {
}
}

Expand Down
40 changes: 40 additions & 0 deletions src/test/kotlin/org/dataloader/KotlinExamples.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.dataloader

import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletableFuture.*

/**
* Some Kotlin code to prove that are JSpecify annotations work here
* as expected in Kotlin land. We don't intend to ue Kotlin in our tests
* or to deliver Kotlin code in the java
*/
class KotlinExamples {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just enough Kotlin - tests only

confirmed the build .jar has no Kotlin classes!


@Test
fun `basic kotlin test of non nullable value types`() {
val dataLoader: DataLoader<String, String> = DataLoaderFactory.newDataLoader { keys -> completedFuture(keys.toList()) }

val cfA = dataLoader.load("A")
val cfB = dataLoader.load("B")

dataLoader.dispatch()

assert(cfA.join().equals("A"))
assert(cfA.join().equals("A"))
}

@Test
fun `basic kotlin test of nullable value types`() {
val dataLoader: DataLoader<String, String?> = DataLoaderFactory.newDataLoader { keys -> completedFuture(keys.toList()) }

val cfA = dataLoader.load("A")
val cfB = dataLoader.load("B")

dataLoader.dispatch()

assert(cfA.join().equals("A"))
assert(cfA.join().equals("A"))
}

}