Randgalt / record-builder

Record builder generator for Java records

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Checker framework's NonNull annotation is ignored

sesamzoo opened this issue · comments

Hi, first of all: thanks a lot for this project.

I wanted to use the option interpretNotNulls with the default regex pattern "(?i)((notnull)|(nonnull)|(nonull))" and the Checker framework's NonNull annotation but the generated code does not contain null checks.

Maybe it's related to the target ElementType.TYPE_USE - I read about this in issue #106 but that's the first time I stumbled across that target type, too.

When the following example's handwritten annotation would be used, the generated builder would contain null-checks.

Simple example (using version 33 of record-builder and version 3.22.0 of Checker framework):

package com.example;

import io.soabase.recordbuilder.core.RecordBuilder;
import org.checkerframework.checker.nullness.qual.NonNull; // <-- @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})

//@interface NonNull {} // <-- this would work

@RecordBuilder
@RecordBuilder.Options(interpretNotNulls = true)
public record RecordWithNonNullAnnotation(@NonNull Object foo) {

}

Generated builder:
relevant builder snippet:

    /**
     * Return a new record instance with all fields set to the current values in this builder
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public RecordWithNonNullAnnotation build() {
        return new RecordWithNonNullAnnotation(foo);
    }

complete builder:

// Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder
package com.example;

import java.util.AbstractMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.annotation.processing.Generated;

@Generated("io.soabase.recordbuilder.core.RecordBuilder")
public class RecordWithNonNullAnnotationBuilder {
    private Object foo;

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private RecordWithNonNullAnnotationBuilder() {
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private RecordWithNonNullAnnotationBuilder(Object foo) {
        this.foo = foo;
    }

    /**
     * Static constructor/builder. Can be used instead of new RecordWithNonNullAnnotation(...)
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static RecordWithNonNullAnnotation RecordWithNonNullAnnotation(Object foo) {
        return new RecordWithNonNullAnnotation(foo);
    }

    /**
     * Return a new builder with all fields set to default Java values
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static RecordWithNonNullAnnotationBuilder builder() {
        return new RecordWithNonNullAnnotationBuilder();
    }

    /**
     * Return a new builder with all fields set to the values taken from the given record instance
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static RecordWithNonNullAnnotationBuilder builder(RecordWithNonNullAnnotation from) {
        return new RecordWithNonNullAnnotationBuilder(from.foo());
    }

    /**
     * Return a "with"er for an existing record instance
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static RecordWithNonNullAnnotationBuilder.With from(RecordWithNonNullAnnotation from) {
        return new _FromWith(from);
    }

    /**
     * Return a stream of the record components as map entries keyed with the component name and the value as the component value
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static Stream<Map.Entry<String, Object>> stream(RecordWithNonNullAnnotation record) {
        return Stream.of(new AbstractMap.SimpleImmutableEntry<>("foo", record.foo()));
    }

    /**
     * Return a new record instance with all fields set to the current values in this builder
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public RecordWithNonNullAnnotation build() {
        return new RecordWithNonNullAnnotation(foo);
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    @Override
    public String toString() {
        return "RecordWithNonNullAnnotationBuilder[foo=" + foo + "]";
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    @Override
    public int hashCode() {
        return Objects.hash(foo);
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    @Override
    public boolean equals(Object o) {
        return (this == o) || ((o instanceof RecordWithNonNullAnnotationBuilder r)
                && Objects.equals(foo, r.foo));
    }

    /**
     * Set a new value for the {@code foo} record component in the builder
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public RecordWithNonNullAnnotationBuilder foo(Object foo) {
        this.foo = foo;
        return this;
    }

    /**
     * Return the current value for the {@code foo} record component in the builder
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public Object foo() {
        return foo;
    }

    /**
     * Add withers to {@code RecordWithNonNullAnnotation}
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public interface With {
        /**
         * Return the current value for the {@code foo} record component in the builder
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        Object foo();

        /**
         * Return a new record builder using the current values
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default RecordWithNonNullAnnotationBuilder with() {
            return new RecordWithNonNullAnnotationBuilder(foo());
        }

        /**
         * Return a new record built from the builder passed to the given consumer
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default RecordWithNonNullAnnotation with(
                Consumer<RecordWithNonNullAnnotationBuilder> consumer) {
            RecordWithNonNullAnnotationBuilder builder = with();
            consumer.accept(builder);
            return builder.build();
        }

        /**
         * Return a new instance of {@code RecordWithNonNullAnnotation} with a new value for {@code foo}
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default RecordWithNonNullAnnotation withFoo(Object foo) {
            return new RecordWithNonNullAnnotation(foo);
        }
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private static final class _FromWith implements RecordWithNonNullAnnotationBuilder.With {
        private final RecordWithNonNullAnnotation from;

        private _FromWith(RecordWithNonNullAnnotation from) {
            this.from = from;
        }

        @Override
        public Object foo() {
            return from.foo();
        }
    }
}

Yes, this is due to ElementType.TYPE_USE - Java's annotation mirrors only put these on the type and not the element. It's a trivial fix to get them to be visible in current behavior. But, #111 will try to achieve a more complete handling of null.

@Randgalt Just to add to this for the null design: Some may not want to use any null handling if they are are using static analysis tools with TYPE_USE.

For example for if you generate methods like:

@NonNull String someField

public Builder with(@NonNull String someField) {
    if (someField == null) { // <-- Eclipse will say this is dead code w/ warning
        throw new NullpointerException(...);
    }
}

Eclipse and I think possibly checkerframework may emit a warning that it is dead code particularly if you directly write the condition above.

I think you can trick Eclipse and Checker with Objects.requireNonNull which I believe record builder already does?

You can of course suppress the warning with an annotation on the method or class with something like @SuppressWarnings("null").

Just something to be aware of if you are not.