1. Overview

From the Java 8 release onwards, it’s possible to compile programs using the so-called Pluggable Type Systems – which can apply stricter checks than the ones applied by the compiler.

We only need to use the annotations provided by the several Pluggable Type Systems available.

In this quick article, we’ll explore the Checker Framework, courtesy of the University of Washington.

2. Maven

To start working with the Checker Framework, we need to first add it into our pom.xml:

<dependency>
    <groupId>org.checkerframework</groupId>
    <artifactId>checker-qual</artifactId>
    <version>3.42.0</version>
</dependency>
<dependency>
    <groupId>org.checkerframework</groupId>
    <artifactId>checker</artifactId>
    <version>3.42.0</version>
</dependency>

The latest version of the libraries can be checked on Maven Central.

The first two dependencies contain the code of The Checker Framework, in which all types have been properly annotated by the developers of The Checker Framework.

We then have to properly tweak the maven-compiler-plugin to use The Checker Framework as a pluggable Type System:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven-compiler-plugin.version}</version>
    <configuration>
        <fork>true</fork>
        <compilerArgument>-Xlint:all</compilerArgument>
        <showWarnings>true</showWarnings>
        <annotationProcessorPaths>
            <path>
                <groupId>org.checkerframework</groupId>
                <artifactId>checker</artifactId>
                <version>${checker.version}</version>
            </path>
        </annotationProcessorPaths>
        <annotationProcessors>
            <annotationProcessor>
                org.checkerframework.checker.nullness.NullnessChecker
            </annotationProcessor>
            <annotationProcessor>
                org.checkerframework.checker.interning.InterningChecker
            </annotationProcessor>
            <annotationProcessor>
                org.checkerframework.checker.fenum.FenumChecker
            </annotationProcessor>
            <annotationProcessor>
                org.checkerframework.checker.formatter.FormatterChecker
            </annotationProcessor>
            <annotationProcessor>
                org.checkerframework.checker.regex.RegexChecker
            </annotationProcessor>
        </annotationProcessors>
        <compilerArgs combine.children="append">
            <arg>-Awarns</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
        </compilerArgs>
    </configuration>
</plugin>

The main point here is the content of the tag. Here we listed all the checkers that we want to run against our sources.

3. Avoiding NullPointerExceptions

The first scenario in which The Checker Framework can help us is, identifying the piece of code where a NullPoinerException could originate:

private static int countArgs(@NonNull String[] args) {
    return args.length;
}

public static void main(@Nullable String[] args) {
    System.out.println(countArgs(args));
}

In the above example, we declared with the @NonNull annotation that the args argument of countArgs() should not be null.

Regardless of this constraint, in main(), we invoke the method passing an argument that can indeed be null, because it’s been annotated with @Nullable.

When we compile the code, The Checker Framework duly warns us that something in our code could be wrong:

[WARNING] /checker-plugin/.../NonNullExample.java:[12,38] [argument.type.incompatible]
 incompatible types in argument.
  found   : null
  required: @Initialized @NonNull String @Initialized @NonNull []

4. Proper Use of Constants as Enumerations

Sometimes we use a series of constants as they were items of an enumeration.

Let’s suppose we need a series of countries and planets. We can then annotate these items with the @Fenum annotation to group all the constants that are part of the same “fake” enumeration:

static final @Fenum("country") String ITALY = "IT";
static final @Fenum("country") String US = "US";
static final @Fenum("country") String UNITED_KINGDOM = "UK";

static final @Fenum("planet") String MARS = "Mars";
static final @Fenum("planet") String EARTH = "Earth";
static final @Fenum("planet") String VENUS = "Venus";

After that, when we write a method that should accept a String that is a “planet”, we can properly annotate the argument:

void greetPlanet(@Fenum("planet") String planet){
    System.out.println("Hello " + planet);
}

By error, we can invoke greetPlanet() with a string that hasn’t been defined as being a possible value for a planet, such:

public static void main(String[] args) {
    obj.greetPlanets(US);
}

The Checker Framework can spot the error:

[WARNING] /checker-plugin/.../FakeNumExample.java:[29,26] [argument.type.incompatible]
 incompatible types in argument.
  found   : @Fenum("country") String
  required: @Fenum("planet") String

5. Regular Expressions

Let’s suppose we know a String variable has to store a regular expression with at least one matching group.

We can leverage the Checker Framework and declare such variables like that:

@Regex(1) private static String FIND_NUMBERS = "\\d*";

This is obviously a potential error because the regular expression we assigned to FIND_NUMBERS does not have any matching group.

Indeed, the Checker Framework will diligently inform us about our error at compile time:

[WARNING] /checker-plugin/.../RegexExample.java:[7,51] [assignment.type.incompatible]
incompatible types in assignment.
  found   : @Regex String
  required: @Regex(1) String

6. Conclusion

The Checker Framework is a useful tool for developers that want to go beyond the standard compiler and improve the correctness of their code.

It’s able to detect, at compile time, several typical errors that can usually only be detected at runtime or even halt compilation by raising a compilation error.

There’re many more standard checks than what we covered in this article; check out the checks available in The Checker Framework official manual here, or even write your own.

As always, the source code for this tutorial, with some more examples, can be found on GitHub.