Skip to content

Commit

Permalink
Use annotation default values with @AutoBuilder and @AutoAnnotation.
Browse files Browse the repository at this point in the history
You can use `@AutoBuilder` to call an `@AutoAnnotation` method. As usual for `@AutoAnnotation`, each parameter of that method corresponds to an element of the annotation. If that element has a default value, the parameter should default to that same value.

Trivial example:
```java
class Example {
  @interface MyAnnotation {
    String value() default "foo";
  }

  @AutoAnnotation
  static MyAnnotation myAnnotation(String value) {
    return new AutoAnnotation_Example_myAnnotation(value);
  }

  @autobuilder(callMethod = "myAnnotation")
  interface MyAnnotationBuilder {
    MyAnnotationBuilder value(String value);
    MyAnnotation build();
  }

  static MyAnnotationBuilder myAnnotationBuilder() {
    return new AutoBuilder_Example_MyAnnotationBuilder();
  }
}
```

In the example, the call `myAnnotationBuilder().build()` has the same effect as `myAnnotationBuilder().value("foo").build()` because of `value() default "foo"`.

RELNOTES=AutoBuilder now uses annotation defaults when building a call to an `@AutoAnnotation` method.
PiperOrigin-RevId: 395114215
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed Sep 6, 2021
1 parent 02ff0f1 commit fb96c83
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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 {
Expand Down
33 changes: 33 additions & 0 deletions value/src/main/java/com/google/auto/value/AutoAnnotation.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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:
*
* <pre>
* 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();
* }
* }
* </pre>
*
* 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -134,7 +137,7 @@ void processType(TypeElement autoBuilderType) {
Map<String, String> 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_");
Expand All @@ -152,7 +155,15 @@ void processType(TypeElement autoBuilderType) {
}

private ImmutableSet<Property> propertySet(
ExecutableElement executable, Map<String, String> propertyToGetterName) {
TypeElement autoBuilderType,
ExecutableElement executable,
Map<String, String> propertyToGetterName) {
boolean autoAnnotation =
MoreElements.getAnnotationMirror(executable, AUTO_ANNOTATION_NAME).isPresent();
ImmutableMap<String, String> 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<VariableElement, String> identifiers =
Expand All @@ -161,18 +172,58 @@ private ImmutableSet<Property> 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<String> builderInitializer) {
String name = var.getSimpleName().toString();
TypeMirror type = var.asType();
Optional<String> 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<String, String> 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<String, String> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ abstract class AutoValueOrBuilderTemplateVars extends AutoValueishTemplateVars {
*
* <ul>
* <li>it is {@code @Nullable} (in which case it defaults to null);
* <li>it is {@code Optional} (in which case it defaults to empty);
* <li>it has a builder initializer (for example it is {@code Optional}, which will have an
* initializer of {@code Optional.empty()});
* <li>it has a property-builder method (in which case it defaults to empty).
* </ul>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,23 +160,48 @@ public static class Property {
private final Optional<String> nullableAnnotation;
private final Optionalish optional;
private final String getter;
private final String builderInitializer; // empty, or with initial ` = `.

Property(
String name,
String identifier,
String type,
TypeMirror typeMirror,
Optional<String> nullableAnnotation,
String getter) {
String getter,
Optional<String> 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}.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down

0 comments on commit fb96c83

Please sign in to comment.