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.