A gradle build with Lombok and Checker Framework

NOTE: The delombok plugin for maven is unmaintained, and making the delombok process work with the Checker Framework gets more complicated. Therefore, it makes sense to have a build working with gradle instead. The gradle plugins used here play well together and are maintained.

Once we have typetools/jdk17u built and ready for our OS (to get the most of the Checker Framework), the following build.gradle can be useful to apply:

  1. Java Google code rules
  2. Lombok/delombok with io.freefair.lombok gradle plugin.
  3. Checker Framework checkers (e.g. NullnessChecker).
plugins {
    id 'java'
    id 'com.github.ben-manes.versions' version '0.51.0'
    id 'io.freefair.lombok' version '6.3.0'
    id 'org.checkerframework' version '0.6.44'
    id 'com.diffplug.spotless' version "7.0.0.BETA2"
}

group = 'org.example'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.functionaljava:functionaljava:5.0'
    implementation 'com.google.guava:guava:33.3.0-jre'
    implementation 'ch.qos.logback:logback-classic:1.5.8'
    implementation 'org.slf4j:slf4j-api:2.1.0-alpha1'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.0-rc1'
    implementation 'com.opencsv:opencsv:5.9'

    compileOnly 'org.checkerframework:checker-qual:3.47.0'
    testCompileOnly 'org.checkerframework:checker-qual:3.47.0'
    checkerFramework 'org.checkerframework:checker:3.47.0'
    implementation 'org.checkerframework:checker-util:3.47.0'

    testImplementation 'org.testng:testng:7.10.2'
    testImplementation 'org.assertj:assertj-core:3.26.3'

    compileOnly 'org.projectlombok:lombok:1.18.34'
    // compileOnly 'org.projectlombok:lombok:1.18.22'
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType(JavaCompile).configureEach {
    options.compilerArgs.addAll([
            '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
            '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
            '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
            '--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED',
            '--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED',
            '--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED',
            '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
            '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
            '--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED',
            '-Xmaxerrs', '10000',
            '-Xmaxwarns', '10000',
            '-Awarns'
    ])
}

spotless {
    java {
        eclipse().configFile("${rootProject.projectDir}/eclipse-java-google-style.xml")
        importOrder('','\\#')
                   .wildcardsLast(false)
                   .semanticSort()
        removeUnusedImports()
        formatAnnotations()
    }
}

checkerFramework {
    checkers = [
        'org.checkerframework.checker.nullness.NullnessChecker'
    ]
}

test {
    useTestNG()
    maxHeapSize = '8G'
}

wrapper {
    gradleVersion = '7.3'
}

The build.gradle above is compatible with Java 17, and the plugins are also compatible with JDK 17. Notice that we are using a JDK 17 compatible version of gradle as well.

Spotless

To call spotless, use:

./gradlew :spotlessApply

(Trivial) example of NullnessChecker usage

The following simple example is meant to check that the build.gradle is working for the NullnessChecker:

package org.example;

import lombok.extern.java.Log;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.testng.annotations.Test;

@Log
public class NullnessChecks {
    @Test
    public void nullCheckOnString() {
        @NonNull String str = null;
        log.info(String.format("%d", str.length()));
    }
}

Executing:

./gradlew clean build

gives

> Task :compileTestJava
warning: [options] --add-opens has no effect at compile time
...NullnessChecks.java:14: warning: [assignment] incompatible types in assignment.

        String str = null;
                     ^
  found   : null (NullType)
  required: @UnknownInitialization @NonNull String

We could also have written:

@Log
public class NullnessChecks {
    @Test
    public void nullCheckOnString() {
        String str = null;
        log.info(String.format("%d", str.length()));
    }
}

that is, not writing the @NonNull annotation, because it is the default. In that case, trying to execute the test task would have given:

./gradlew test

> Task :compileTestJava
...NullnessChecks.java:14: warning: [dereference.of.nullable] dereference of possibly-null reference str

        log.info(String.format("%d", str.length()));
                                     ^
1 warning

> Task :test FAILED

Gradle suite > Gradle test > org.example.NullnessChecks > nullCheckOnString FAILED
    java.lang.NullPointerException at NullnessChecks.java:14

1 test completed, 1 failed

Date
September 21, 2024