traute

Enhances java sources compilation in a way to insert null-checks into generated *.class files

View the Project on GitHub harmonysoft-tech/traute

Table of Contents

1. License

See the LICENSE file for license rights and limitations (MIT).

2. Overview

This is a Java Compiler plugin which enhances generated *.class files by inserting null-checks based on source code annotations.

See the main project page for the rationale to have such an instrument. Also be aware of alternatives.

3. Features

Following instrumentations types are supported now:

4. Example

Consider a source code below:

@NotNull
public Integer add(@NotNull Integer a, @NotNull Integer b) {
    return a + b;
}

The plugin modifies resulting byte code as if the source looked like this:

@NotNull
public Integer add(@NotNull Integer a, @NotNull Integer b) {
    if (a == null) {
        throw new NullPointerException("Argument 'a' of the method 'add()' is marked by @NotNull but got null for it");
    }
    if (b == null) {
        throw new NullPointerException("Argument 'b' of the method 'add()' is marked by @NotNull but got null for it");
    }
    Integer tmpVar = a + b;
    if (tmpVar == null) {
        throw new NullPointerException("Detected an attempt to return null from a method marked by @NotNull");
    }
    return tmpVar;
}

5. Limitations

The plugin works with JDK8 or later - Compiler Plugin API is introduced only in java8.

6. Usage

  1. Put the plugin’s jar to compiler’s classpath
  2. Add -Xplugin:Traute option to the javac command line

Example:

javac -cp src/main/java\
:~/.gradle/caches/modules-2/files-2.1/tech.harmonysoft:traute-javac-plugin/1.0.7/7a00452c350de0fb80ecbcecfb8ce0145c46141e/traute-javac-plugin-1.0.0.jar \
-Xplugin:Traute \
org/MyClass.java

That makes compiler involve the plugin into the processing which, in turn, adds null-checks to the *.class file if necessary.

It’s also possible to specify a number of plugin-specific options (see below).

7. Settings

All plugin settings are delivered through the -A command line switch. See javac documentation for more details.

7.1. NotNull Annotations

The plugin inserts null-checks for method parameters and return values marked by the annotations below by default:

It’s possible to define a custom list of annotations to use through the traute.annotations.not.null option.

Example:

7.2. NotNullByDefault Annotations

It’s possible to specify that method parameters/return types are not null by default, e.g. consider a package-info.java file with the content like below:

@ParametersAreNonnullByDefault
package my.company;

import javax.annotation.ParametersAreNonnullByDefault;

Here my.company package is marked by the ParametersAreNonnullByDefault annotation. That means that all method parameters for classes in the target package are treated as if they are marked by NotNull annotation (except those which are explicitly marked by Nullable annotations).

Traute supports such NotNullByDefault annotations on package, class and method level.

We can customize that annotations through the traute.annotations.not.null.by.default. option prefix followed by the instrumentation type.

Example:

javac -cp <classpath> -Xplugin:Traute -Atraute.annotations.not.null.by.default.parameter=my.custom.NotNullByDefault

This instructs the plugin to use my.custom.NotNullByDefault annotation as a NotNullByDefault during method parameters instrumentation.

It’s possible to specify more than one annotation such separated by the colon (:).

Following annotations are used by default for processing method parameters:

Following annotations are used by default for processing method return values:

7.3. Nullable Annotations

As mentioned above, it’s possible to specify that target method parameters/return values are NotNullByDefault. However, we might want to allow null in particular use-cases. Such method parameters/return types can be marked by a Nullable annotation then.

Consider an example below:

@javax.annotation.ParametersAreNonnullByDefault
public void test(Object arg1, Object arg2, @Nullable Object arg3) {
}

When this code is compiled, null checks are generated for arg1 and arg2 but not for arg3:

@javax.annotation.ParametersAreNonnullByDefault
public void test(Object arg1, Object arg2, @Nullable Object arg3) {
    if (arg1 == null) {
        throw new NullPointerException("'arg1' must not be null");
    }
    if (arg2 == null) {
        throw new NullPointerException("'arg2' must not be null");
    }
}

It’s possible to specify Nullable annotations to use through the traute.annotations.nullable option:

javac -cp <classpath> -Xplugin:Traute -Atraute.annotations.nullable=mycompany.util.Nullable <classes-to-compile>

Multiple annotations separated by colon (:) might be provided.

Following annotations are used by default:

7.4. Instrumentation Types

Following instrumentation types are supported now:

Even though they are thoroughly tested it’s not possible to exclude a possibility that particular use-case is not covered (e.g. we encountered tricky situations like here). That’s why we allow to skip particular instrumentations through the traute.instrumentations option.

Example:

javac -cp <classpath> -Xplugin:Traute -Atraute.instrumentations=parameter <classes-to-compile>

This effectively disables return instrumentation.

7.5. Exception to Throw

NullPointerException is thrown in case of a failed check by default. However, it’s possible to specify another exceptions to be thrown. It’s defined through the traute.exception. prefix followed by the instrumentation type.

Example:

javac -cp <classpath> -Xplugin:Traute -Atraute.exception.parameter=IllegalArgumentException -Atraute.exception.return=IllegalStateException

This specifies an IllegalArgumentException to be thrown when a null is received for a @NotNull method parameter and IllegalStateException to be thrown when a method marked by @NotNull tries to return null.

7.6. Exception Text

The plugin uses pre-defined error text in null-checks, however, it’s possible to customize that. It’s defined through the traute.failure.text. option prefix followed by the instrumentation type.

It’s possible to use substitutions in the custom text value. They are defined through the ${VAR_NAME} syntax. Following variables are supported now:

It’s also possible to apply functions to the substituted variables:

Example:

javac -cp <classpath> -Xplugin:Traute '-Atraute.failure.text.parameter=${capitalize(PARAMETER_NAME)} must not be null'

Here the plugin generates a check like below:

public void test(@NotNull Object myArg) {
    if (myArg == null) {
        throw new NullPointerException("MyArg must not be null");
    }
}

7.7. Logging

The plugin logs only custom options by default:

javac -cp <classpath> -Xplugin:Traute -Atraute.instrumentations=parameter <classes-to-compile>

Compiler output:

[Traute plugin]: using the following instrumentations: [parameter]

It’s possible to turn on verbose mode through the traute.log.verbose option to get detailed information about performed instrumentations.

Example:

javac -cp <classpath> -Xplugin:Traute -Atraute.log.verbose=true <classes-to-compile>

Output:

[Traute plugin]: 'verbose mode' is on
[Traute plugin]: added a null-check for argument 'i2' in the method org.Test.test()
[Traute plugin]: added a null-check for argument 'i1' in the method org.Test.test()
[Traute plugin]: added a null-check for 'return' expression in method org.Test.test()
[Traute plugin]: added 3 instrumentations to the class /Users/denis/sample/src/main/java/org/Test.java - METHOD_PARAMETER: 2, METHOD_RETURN: 1
[Traute plugin]: added a null-check for argument 'i1' in the method org.Test2.test()
[Traute plugin]: added 1 instrumentation to the class /Users/denis/sample/src/main/java/org/Test2.java - METHOD_PARAMETER: 1

7.8. Log Location

The plugin logs into compiler’s output by default. However, it’s possible to configure a custom file to hold that data. Corresponding option is traute.log.file.

Example:

javac -cp <classpath> -Xplugin:Traute -Atraute.log.file=/home/me/traute.log

The logs will be written into /home/me/traute.log

8. Evolution

Current feature set is a must-have for runtime null-checks, however, it’s possible to extend it. Here are some ideas on what might be done:

9. Implementation

Implementation details are described in this blog post.