Randgalt / record-builder

Record builder generator for Java records
Apache License 2.0
756 stars 55 forks source link

`TYPE_USE` annotations were being ignored #115

Closed Randgalt closed 2 years ago

Randgalt commented 2 years ago

Java's DAG for annotations processors doesn't contain TYPE_USE annotations on the Element for some reason. However, they are on the type. So, use the type instead.

Note due to limitations of JavaPoet this doesn't fix TYPE_USE annotations on parameterized types or array components. If we want to address those we will need changes in JavaPoet which has been dormant for a very long time.

Fixes #113 Relates to #111

Randgalt commented 2 years ago

cc @sesamzoo and @agentgt

Randgalt commented 2 years ago

FYI - for the standard @RecordBuilder you get:

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

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 MyRecordBuilder {
    private String nonNullS;

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

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private MyRecordBuilder(String nonNullS) {
        this.nonNullS = nonNullS;
    }

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

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

    /**
     * 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 MyRecordBuilder builder(MyRecord from) {
        return new MyRecordBuilder(from.nonNullS());
    }

    /**
     * Return a "with"er for an existing record instance
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static MyRecordBuilder.With from(MyRecord 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(MyRecord record) {
        return Stream.of(new AbstractMap.SimpleImmutableEntry<>("nonNullS", record.nonNullS()));
    }

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

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

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

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

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

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

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

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

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

        /**
         * Return a new instance of {@code MyRecord} with a new value for {@code nonNullS}
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default MyRecord withNonNullS(@TypeUseNonNull String nonNullS) {
            return new MyRecord(nonNullS);
        }
    }

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

        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        private _FromWith(MyRecord from) {
            this.from = from;
        }

        @Override
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        public String nonNullS() {
            return from.nonNullS();
        }
    }
}
Randgalt commented 2 years ago

For RecordBuilderFull you get:

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

import io.soabase.recordbuilder.core.RecordBuilderGenerated;
import java.util.AbstractMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import javax.annotation.processing.Generated;

@Generated("io.soabase.recordbuilder.core.RecordBuilder")
@RecordBuilderGenerated
public class MyFullRecordBuilder {
    private String nonNullS;

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

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private MyFullRecordBuilder(String nonNullS) {
        this.nonNullS = nonNullS;
    }

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

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

    /**
     * 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 MyFullRecordBuilder builder(MyFullRecord from) {
        return new MyFullRecordBuilder(from.nonNullS());
    }

    /**
     * Return a "with"er for an existing record instance
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static MyFullRecordBuilder.With from(MyFullRecord 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(MyFullRecord record) {
        return Stream.of(new AbstractMap.SimpleImmutableEntry<>("nonNullS", record.nonNullS()));
    }

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

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

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

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

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

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

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

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

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

        /**
         * Return a new instance of {@code MyFullRecord} with a new value for {@code nonNullS}
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default MyFullRecord withNonNullS(@TypeUseNonNull String nonNullS) {
            return new MyFullRecord(nonNullS);
        }

        /**
         * Map record components into a new object
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default <R> R map(Function<R> proc) {
            return proc.apply(nonNullS());
        }

        /**
         * Perform an operation on record components
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default void accept(Consumer proc) {
            proc.apply(nonNullS());
        }

        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        @FunctionalInterface
        interface Function<R> {
            R apply(@TypeUseNonNull String nonNullS);
        }

        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        @FunctionalInterface
        interface Consumer {
            void apply(@TypeUseNonNull String nonNullS);
        }
    }

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

        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        private _FromWith(MyFullRecord from) {
            this.from = from;
        }

        @Override
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        public String nonNullS() {
            return from.nonNullS();
        }
    }
}
Randgalt commented 2 years ago

@sesamzoo and @agentgt any comments before I merge?

agentgt commented 2 years ago

I’ll have a look Monday. Sorry I was sick the last few days.

sesamzoo commented 2 years ago

@Randgalt, with this PR my example from #113 works as expected.

agentgt commented 2 years ago

@Randgalt it looks right. I have not pulled the code yet and tested things like arrays and inner classes but it looks correct. Even if it doesn't handle those cases yet this looks like the correct behavior of propagating the type annotations.

BTW you might want to test and or be careful with the Eclipse compiler annotation processor. IIRC it has an issue where it will propagate @NonNull-like TYPE_USE annotations even if they are not on the type! Basically the nonnull analysis part of the compiler inserts it in. This is kind of a bug with Eclipse. The irony is annotation processors that ignore TYPE_USE do not have this problem but those that do can have issues. It is possible @stephan-herrmann has fixed this.

Anyway Google Auto Value is one of the few projects I know that does deal with TYPE_USE annotations correctly (and now this project will as well 😄 ) and they have issues with Eclipse (see):

I'm not saying you need to deal with it but just be aware of it.