diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java index 952edaac3b..dba8199207 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java @@ -149,6 +149,12 @@ static MyAnnotation myAnnotation(String value, Truthiness truthiness) { return new AutoAnnotation_AutoBuilderTest_myAnnotation(value, truthiness); } + // This method has parameters for all the annotation elements. + @AutoAnnotation + static MyAnnotation myAnnotationAll(String value, int id, Truthiness truthiness) { + return new AutoAnnotation_AutoBuilderTest_myAnnotationAll(value, id, truthiness); + } + @AutoBuilder(callMethod = "myAnnotation") interface MyAnnotationBuilder { MyAnnotationBuilder value(String x); @@ -159,12 +165,28 @@ interface MyAnnotationBuilder { } static MyAnnotationBuilder myAnnotationBuilder() { - return new AutoBuilder_AutoBuilderTest_MyAnnotationBuilder() - .truthiness(MyAnnotation.DEFAULT_TRUTHINESS); + return new AutoBuilder_AutoBuilderTest_MyAnnotationBuilder(); + } + + @AutoBuilder(callMethod = "myAnnotationAll") + interface MyAnnotationAllBuilder { + MyAnnotationAllBuilder value(String x); + + MyAnnotationAllBuilder id(int x); + + MyAnnotationAllBuilder truthiness(Truthiness x); + + MyAnnotation build(); + } + + static MyAnnotationAllBuilder myAnnotationAllBuilder() { + return new AutoBuilder_AutoBuilderTest_MyAnnotationAllBuilder(); } @Test public void simpleAutoAnnotation() { + // We haven't supplied a value for `truthiness`, so AutoBuilder should use the default one in + // the annotation. MyAnnotation annotation1 = myAnnotationBuilder().value("foo").build(); assertThat(annotation1.value()).isEqualTo("foo"); assertThat(annotation1.id()).isEqualTo(MyAnnotation.DEFAULT_ID); @@ -174,6 +196,15 @@ public void simpleAutoAnnotation() { assertThat(annotation2.value()).isEqualTo("bar"); assertThat(annotation2.id()).isEqualTo(MyAnnotation.DEFAULT_ID); assertThat(annotation2.truthiness()).isEqualTo(Truthiness.TRUTHY); + + MyAnnotation annotation3 = myAnnotationAllBuilder().value("foo").build(); + MyAnnotation annotation4 = + myAnnotationAllBuilder() + .value("foo") + .id(MyAnnotation.DEFAULT_ID) + .truthiness(MyAnnotation.DEFAULT_TRUTHINESS) + .build(); + assertThat(annotation3).isEqualTo(annotation4); } static class Overload { diff --git a/value/src/main/java/com/google/auto/value/AutoAnnotation.java b/value/src/main/java/com/google/auto/value/AutoAnnotation.java index d36d8e285f..c6fab24043 100644 --- a/value/src/main/java/com/google/auto/value/AutoAnnotation.java +++ b/value/src/main/java/com/google/auto/value/AutoAnnotation.java @@ -71,6 +71,39 @@ * parameter corresponding to an array-valued annotation member, and the implementation of each such * member will also return a clone of the array. * + *

If your annotation has many elements, you may consider using {@code @AutoBuilder} to make it + * easier to construct instances. In that case, {@code default} values from the annotation will + * become default values for the parameters of the {@code @AutoAnnotation} method. For example: + * + *

+ * class Example {
+ *   {@code @interface} MyAnnotation {
+ *     String name() default "foo";
+ *     int number() default 23;
+ *   }
+ *
+ *   {@code @AutoAnnotation}
+ *   static MyAnnotation myAnnotation(String value) {
+ *     return new AutoAnnotation_Example_myAnnotation(value);
+ *   }
+ *
+ *   {@code @AutoBuilder(callMethod = "myAnnotation")}
+ *   interface MyAnnotationBuilder {
+ *     MyAnnotationBuilder name(String name);
+ *     MyAnnotationBuilder number(int number);
+ *     MyAnnotation build();
+ *   }
+ *
+ *   static MyAnnotationBuilder myAnnotationBuilder() {
+ *     return new AutoBuilder_Example_MyAnnotationBuilder();
+ *   }
+ * }
+ * 
+ * + * Here, {@code myAnnotationBuilder().build()} is the same as {@code + * myAnnotationBuilder().name("foo").number(23).build()} because those are the defaults in the + * annotation definition. + * * @author emcmanus@google.com (Éamonn McManus) */ @Target(ElementType.METHOD) diff --git a/value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java b/value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java index ed986ab7b6..0c8b8f0f55 100644 --- a/value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java +++ b/value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java @@ -130,13 +130,13 @@ public Void visitType(TypeMirror classConstant, StringBuilder sb) { private static class InitializerSourceFormVisitor extends SourceFormVisitor { private final ProcessingEnvironment processingEnv; private final String memberName; - private final Element context; + private final Element errorContext; InitializerSourceFormVisitor( - ProcessingEnvironment processingEnv, String memberName, Element context) { + ProcessingEnvironment processingEnv, String memberName, Element errorContext) { this.processingEnv = processingEnv; this.memberName = memberName; - this.context = context; + this.errorContext = errorContext; } @Override @@ -148,7 +148,7 @@ public Void visitAnnotation(AnnotationMirror a, StringBuilder sb) { "@AutoAnnotation cannot yet supply a default value for annotation-valued member '" + memberName + "'", - context); + errorContext); sb.append("null"); return null; } @@ -209,9 +209,9 @@ static String sourceFormForInitializer( AnnotationValue annotationValue, ProcessingEnvironment processingEnv, String memberName, - Element context) { + Element errorContext) { SourceFormVisitor visitor = - new InitializerSourceFormVisitor(processingEnv, memberName, context); + new InitializerSourceFormVisitor(processingEnv, memberName, errorContext); StringBuilder sb = new StringBuilder(); visitor.visit(annotationValue, sb); return sb.toString(); diff --git a/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java index b6a578fcf0..53fe836db2 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java @@ -20,6 +20,7 @@ import static com.google.auto.common.MoreStreams.toImmutableList; import static com.google.auto.common.MoreStreams.toImmutableSet; import static com.google.auto.value.processor.AutoValueProcessor.OMIT_IDENTIFIERS_OPTION; +import static com.google.auto.value.processor.ClassNames.AUTO_ANNOTATION_NAME; import static com.google.auto.value.processor.ClassNames.AUTO_BUILDER_NAME; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toCollection; @@ -38,6 +39,7 @@ import com.google.common.base.Ascii; import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import java.lang.reflect.Field; @@ -60,6 +62,7 @@ import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import net.ltgt.gradle.incap.IncrementalAnnotationProcessor; import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType; @@ -134,7 +137,7 @@ void processType(TypeElement autoBuilderType) { Map propertyToGetterName = Maps.transformValues(classifier.builderGetters(), PropertyGetter::getName); AutoBuilderTemplateVars vars = new AutoBuilderTemplateVars(); - vars.props = propertySet(executable, propertyToGetterName); + vars.props = propertySet(autoBuilderType, executable, propertyToGetterName); builder.defineVars(vars, classifier); vars.identifiers = !processingEnv.getOptions().containsKey(OMIT_IDENTIFIERS_OPTION); String generatedClassName = generatedClassName(autoBuilderType, "AutoBuilder_"); @@ -152,7 +155,15 @@ void processType(TypeElement autoBuilderType) { } private ImmutableSet propertySet( - ExecutableElement executable, Map propertyToGetterName) { + TypeElement autoBuilderType, + ExecutableElement executable, + Map propertyToGetterName) { + boolean autoAnnotation = + MoreElements.getAnnotationMirror(executable, AUTO_ANNOTATION_NAME).isPresent(); + ImmutableMap builderInitializers = + autoAnnotation + ? autoAnnotationInitializers(autoBuilderType, executable) + : ImmutableMap.of(); // Fix any parameter names that are reserved words in Java. Java source code can't have // such parameter names, but Kotlin code might, for example. Map identifiers = @@ -161,18 +172,58 @@ private ImmutableSet propertySet( fixReservedIdentifiers(identifiers); return executable.getParameters().stream() .map( - v -> - newProperty( - v, identifiers.get(v), propertyToGetterName.get(v.getSimpleName().toString()))) + v -> { + String name = v.getSimpleName().toString(); + return newProperty( + v, + identifiers.get(v), + propertyToGetterName.get(name), + Optional.ofNullable(builderInitializers.get(name))); + }) .collect(toImmutableSet()); } - private Property newProperty(VariableElement var, String identifier, String getterName) { + private Property newProperty( + VariableElement var, + String identifier, + String getterName, + Optional builderInitializer) { String name = var.getSimpleName().toString(); TypeMirror type = var.asType(); Optional nullableAnnotation = nullableAnnotationFor(var, var.asType()); return new Property( - name, identifier, TypeEncoder.encode(type), type, nullableAnnotation, getterName); + name, + identifier, + TypeEncoder.encode(type), + type, + nullableAnnotation, + getterName, + builderInitializer); + } + + private ImmutableMap autoAnnotationInitializers( + TypeElement autoBuilderType, ExecutableElement autoAnnotationMethod) { + // We expect the return type of an @AutoAnnotation method to be an annotation type. If it isn't, + // AutoAnnotation will presumably complain, so we don't need to complain further. + TypeMirror returnType = autoAnnotationMethod.getReturnType(); + if (!returnType.getKind().equals(TypeKind.DECLARED)) { + return ImmutableMap.of(); + } + // This might not actually be an annotation (if the code is wrong), but if that's the case we + // just won't see any contained ExecutableElement where getDefaultValue() returns something. + TypeElement annotation = MoreTypes.asTypeElement(returnType); + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (ExecutableElement method : methodsIn(annotation.getEnclosedElements())) { + AnnotationValue defaultValue = method.getDefaultValue(); + if (defaultValue != null) { + String memberName = method.getSimpleName().toString(); + builder.put( + memberName, + AnnotationOutput.sourceFormForInitializer( + defaultValue, processingEnv, memberName, autoBuilderType)); + } + } + return builder.build(); } private ExecutableElement findExecutable( diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueOrBuilderTemplateVars.java b/value/src/main/java/com/google/auto/value/processor/AutoValueOrBuilderTemplateVars.java index 86cf4974aa..9fbc16523e 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueOrBuilderTemplateVars.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueOrBuilderTemplateVars.java @@ -109,7 +109,8 @@ abstract class AutoValueOrBuilderTemplateVars extends AutoValueishTemplateVars { * *
    *
  • it is {@code @Nullable} (in which case it defaults to null); - *
  • it is {@code Optional} (in which case it defaults to empty); + *
  • it has a builder initializer (for example it is {@code Optional}, which will have an + * initializer of {@code Optional.empty()}); *
  • it has a property-builder method (in which case it defaults to empty). *
*/ diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueishProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoValueishProcessor.java index 9b8605524b..ea08557552 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueishProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueishProcessor.java @@ -160,6 +160,7 @@ public static class Property { private final Optional nullableAnnotation; private final Optionalish optional; private final String getter; + private final String builderInitializer; // empty, or with initial ` = `. Property( String name, @@ -167,16 +168,40 @@ public static class Property { String type, TypeMirror typeMirror, Optional nullableAnnotation, - String getter) { + String getter, + Optional maybeBuilderInitializer) { this.name = name; this.identifier = identifier; this.type = type; this.typeMirror = typeMirror; this.nullableAnnotation = nullableAnnotation; this.optional = Optionalish.createIfOptional(typeMirror); + this.builderInitializer = + maybeBuilderInitializer.isPresent() + ? " = " + maybeBuilderInitializer.get() + : builderInitializer(); this.getter = getter; } + /** + * Returns the appropriate initializer for a builder property. Builder properties are never + * primitive; if the built property is an {@code int} the builder property will be an {@code + * Integer}. So the default value for a builder property will be null unless there is an + * initializer. The caller of the constructor may have supplied an initializer, but otherwise we + * supply one only if this property is an {@code Optional} and is not {@code @Nullable}. In that + * case the initializer sets it to {@code Optional.empty()}. + */ + private String builderInitializer() { + if (nullableAnnotation.isPresent()) { + return ""; + } + Optionalish optional = Optionalish.createIfOptional(typeMirror); + if (optional == null) { + return ""; + } + return " = " + optional.getEmpty(); + } + /** * Returns the name of the property as it should be used when declaring identifiers (fields and * parameters). If the original getter method was {@code foo()} then this will be {@code foo}. @@ -218,6 +243,14 @@ public Optionalish getOptional() { return optional; } + /** + * Returns a string to be used as an initializer for a builder field for this property, + * including the leading {@code =}, or an empty string if there is no explicit initializer. + */ + public String getBuilderInitializer() { + return builderInitializer; + } + /** * Returns the string to use as a method annotation to indicate the nullability of this * property. It is either the empty string, if the property is not nullable, or an annotation @@ -266,7 +299,8 @@ public static class GetterProperty extends Property { type, method.getReturnType(), nullableAnnotation, - method.getSimpleName().toString()); + method.getSimpleName().toString(), + Optional.empty()); this.method = method; this.fieldAnnotations = fieldAnnotations; this.methodAnnotations = methodAnnotations; diff --git a/value/src/main/java/com/google/auto/value/processor/BuilderSpec.java b/value/src/main/java/com/google/auto/value/processor/BuilderSpec.java index 9f45d1720c..dfdbb8c7d3 100644 --- a/value/src/main/java/com/google/auto/value/processor/BuilderSpec.java +++ b/value/src/main/java/com/google/auto/value/processor/BuilderSpec.java @@ -333,7 +333,7 @@ void defineVars(AutoValueOrBuilderTemplateVars vars, BuilderMethodClassifier vars.builderRequiredProperties = vars.props.stream() .filter(p -> !p.isNullable()) - .filter(p -> p.getOptional() == null) + .filter(p -> p.getBuilderInitializer().isEmpty()) .filter(p -> !vars.builderPropertyBuilders.containsKey(p.getName())) .collect(toImmutableSet()); } diff --git a/value/src/main/java/com/google/auto/value/processor/builder.vm b/value/src/main/java/com/google/auto/value/processor/builder.vm index 630330cafa..950f9838f1 100644 --- a/value/src/main/java/com/google/auto/value/processor/builder.vm +++ b/value/src/main/java/com/google/auto/value/processor/builder.vm @@ -40,7 +40,7 @@ class ${builderName}${builderFormalTypes} ## #if ($p.kind.primitive) - private $types.boxedClass($p.typeMirror).simpleName $p; + private $types.boxedClass($p.typeMirror).simpleName $p $p.builderInitializer; #else @@ -54,7 +54,7 @@ class ${builderName}${builderFormalTypes} ## #end - private $p.type $p #if ($p.optional && !$p.nullable) = $p.optional.empty #end ; + private $p.type $p $p.builderInitializer; #end #end