Skip to content

Commit

Permalink
Allow AutoBuilder to build annotation implementations directly.
Browse files Browse the repository at this point in the history
RELNOTES=AutoBuilder can now be used to build annotation implementations directly.
PiperOrigin-RevId: 431017277
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed Feb 25, 2022
1 parent 36553ad commit 196c810
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,63 @@ public void simpleAutoAnnotation() {
assertThat(annotation3).isEqualTo(annotation4);
}

@AutoBuilder(ofClass = MyAnnotation.class)
public interface MyAnnotationSimpleBuilder {
MyAnnotationSimpleBuilder value(String x);
MyAnnotationSimpleBuilder id(int x);
MyAnnotationSimpleBuilder truthiness(Truthiness x);
MyAnnotation build();
}

public static MyAnnotationSimpleBuilder myAnnotationSimpleBuilder() {
return new AutoBuilder_AutoBuilderTest_MyAnnotationSimpleBuilder();
}

@Test
public void buildWithoutAutoAnnotation() {
// We don't set a value for `id` or `truthiness`, so AutoBuilder should use the default ones in
// the annotation.
MyAnnotation annotation1 = myAnnotationSimpleBuilder().value("foo").build();
assertThat(annotation1.value()).isEqualTo("foo");
assertThat(annotation1.id()).isEqualTo(MyAnnotation.DEFAULT_ID);
assertThat(annotation1.truthiness()).isEqualTo(MyAnnotation.DEFAULT_TRUTHINESS);

// Now we set `truthiness` but still not `id`.
MyAnnotation annotation2 =
myAnnotationSimpleBuilder().value("bar").truthiness(Truthiness.TRUTHY).build();
assertThat(annotation2.value()).isEqualTo("bar");
assertThat(annotation2.id()).isEqualTo(MyAnnotation.DEFAULT_ID);
assertThat(annotation2.truthiness()).isEqualTo(Truthiness.TRUTHY);

// All three elements set explicitly.
MyAnnotation annotation3 =
myAnnotationSimpleBuilder().value("foo").id(23).truthiness(Truthiness.TRUTHY).build();
assertThat(annotation3.value()).isEqualTo("foo");
assertThat(annotation3.id()).isEqualTo(23);
assertThat(annotation3.truthiness()).isEqualTo(Truthiness.TRUTHY);
}

// This builder doesn't have a setter for the `truthiness` element, so the annotations it builds
// should always get the default value.
@AutoBuilder(ofClass = MyAnnotation.class)
public interface MyAnnotationSimplerBuilder {
MyAnnotationSimplerBuilder value(String x);
MyAnnotationSimplerBuilder id(int x);
MyAnnotation build();
}

public static MyAnnotationSimplerBuilder myAnnotationSimplerBuilder() {
return new AutoBuilder_AutoBuilderTest_MyAnnotationSimplerBuilder();
}

@Test
public void buildWithoutAutoAnnotation_noSetterForElement() {
MyAnnotation annotation = myAnnotationSimplerBuilder().value("foo").id(23).build();
assertThat(annotation.value()).isEqualTo("foo");
assertThat(annotation.id()).isEqualTo(23);
assertThat(annotation.truthiness()).isEqualTo(MyAnnotation.DEFAULT_TRUTHINESS);
}

static class Overload {
final int anInt;
final String aString;
Expand Down
13 changes: 4 additions & 9 deletions value/src/main/java/com/google/auto/value/AutoAnnotation.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@
* 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:
* <p>If your annotation has many elements, you may consider using {@code @AutoBuilder} instead of
* {@code @AutoAnnotation} to make it easier to construct instances. In that case, {@code default}
* values from the annotation will become default values for the values in the builder. For example:
*
* <pre>
* class Example {
Expand All @@ -82,12 +82,7 @@
* int number() default 23;
* }
*
* {@code @AutoAnnotation}
* static MyAnnotation myAnnotation(String value) {
* return new AutoAnnotation_Example_myAnnotation(value);
* }
*
* {@code @AutoBuilder(callMethod = "myAnnotation")}
* {@code @AutoBuilder(ofClass = MyAnnotation.class)}
* interface MyAnnotationBuilder {
* MyAnnotationBuilder name(String name);
* MyAnnotationBuilder number(int number);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.auto.value.processor;

import com.google.auto.value.processor.AutoValueishProcessor.Property;
import com.google.common.collect.ImmutableSet;
import com.google.escapevelocity.Template;

/** The variables to substitute into the autobuilderannotation.vm template. */
class AutoBuilderAnnotationTemplateVars extends TemplateVars {
private static final Template TEMPLATE = parsedTemplateForResource("autobuilderannotation.vm");

/** Package of generated class. */
String pkg;

/** The encoding of the {@code Generated} class. Empty if the class is not available. */
String generated;

/** The name of the class to generate. */
String className;

/**
* The {@linkplain TypeEncoder#encode encoded} name of the annotation type that the generated code
* will build.
*/
String annotationType;

/**
* The {@linkplain TypeEncoder#encode encoded} name of the {@code @AutoBuilder} type that users
* will call to build this annotation.
*/
String autoBuilderType;

/**
* The "properties" that the builder will build. These are really just names and types, being the
* names and types of the annotation elements.
*/
ImmutableSet<Property> props;

@Override
Template parsedTemplate() {
return TEMPLATE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.auto.value.processor;

import static com.google.auto.common.GeneratedAnnotations.generatedAnnotation;
import static com.google.auto.common.MoreElements.getLocalAndInheritedMethods;
import static com.google.auto.common.MoreElements.getPackage;
import static com.google.auto.common.MoreStreams.toImmutableList;
Expand Down Expand Up @@ -78,6 +79,7 @@
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING)
public class AutoBuilderProcessor extends AutoValueishProcessor {
private static final String ALLOW_OPTION = "com.google.auto.value.AutoBuilderIsUnstable";
private static final String AUTO_ANNOTATION_CLASS_PREFIX = "AutoBuilderAnnotation_";

public AutoBuilderProcessor() {
super(AUTO_BUILDER_NAME, /* appliesToInterfaces= */ true);
Expand All @@ -96,6 +98,55 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
javaLangVoid = elementUtils().getTypeElement("java.lang.Void").asType();
}

// The handling of @AutoBuilder to generate annotation implementations needs some explanation.
// Suppose we have this:
//
// public class Annotations {
// @interface MyAnnot {...}
//
// @AutoBuilder(ofClass = MyAnnot.class)
// public interface MyAnnotBuilder {
// ...
// MyAnnot build();
// }
//
// public static MyAnnotBuilder myAnnotBuilder() {
// return new AutoBuilder_Annotations_MyAnnotBuilder();
// }
// }
//
// Then we will detect that the ofClass type is an annotation. Since annotations can have neither
// constructors nor static methods, we know this isn't a regular @AutoBuilder. We want to
// generate an implementation of the MyAnnot annotation, and we know we can do that if we have a
// suitable @AutoAnnotation method. So we generate:
//
// class AutoBuilderAnnotation_Annotations_MyAnnotBuilder {
// @AutoAnnotation
// static MyAnnot newAnnotation(...) {
// return new AutoAnnotation_AutoBuilderAnnotation_Annotations_MyAnnotBuilder_newAnnotation(
// ...);
// }
// }
//
// We also "defer" MyAnnotBuilder so that it will be considered again on the next round. At that
// point the method AutoBuilderAnnotation_Annotations_MyAnnotBuilder.newAnnotation will exist, and
// we just need to tweak the handling of MyAnnotBuilder so that it behaves as if it were:
//
// @AutoBuilder(
// callMethod = newAnnotation,
// ofClass = AutoBuilderAnnotation_Annotations_MyAnnotBuilder.class)
// interface MyAnnotBuilder {...}
//
// Using AutoAnnotation and AutoBuilder together you'd write
//
// @AutoAnnotation static MyAnnot newAnnotation(...) { ... }
//
// @AutoBuilder(callMethod = "newAnnotation", ofClass = Some.class)
// interface MyAnnotBuilder { ... }
//
// If you set ofClass to an annotation class, AutoBuilder generates the @AutoAnnotation method for
// you and then acts as if your @AutoBuilder annotation pointed to it.

@Override
void processType(TypeElement autoBuilderType) {
if (processingEnv.getOptions().containsKey(ALLOW_OPTION)) {
Expand All @@ -107,16 +158,32 @@ void processType(TypeElement autoBuilderType) {
TypeElement ofClass = getOfClass(autoBuilderType, autoBuilderAnnotation);
checkModifiersIfNested(ofClass, autoBuilderType, "AutoBuilder ofClass");
String callMethod = findCallMethodValue(autoBuilderAnnotation);
if (ofClass.getKind() == ElementKind.ANNOTATION_TYPE) {
buildAnnotation(autoBuilderType, ofClass, callMethod);
} else {
processType(autoBuilderType, ofClass, callMethod);
}
}

private void processType(TypeElement autoBuilderType, TypeElement ofClass, String callMethod) {
ImmutableSet<ExecutableElement> methods =
abstractMethodsIn(
getLocalAndInheritedMethods(autoBuilderType, typeUtils(), elementUtils()));
ExecutableElement executable = findExecutable(ofClass, callMethod, autoBuilderType, methods);
BuilderSpec builderSpec = new BuilderSpec(ofClass, processingEnv, errorReporter());
BuilderSpec.Builder builder = builderSpec.new Builder(autoBuilderType);
TypeMirror builtType = builtType(executable);
ImmutableMap<String, String> propertyInitializers =
propertyInitializers(autoBuilderType, executable);
Optional<BuilderMethodClassifier<VariableElement>> maybeClassifier =
BuilderMethodClassifierForAutoBuilder.classify(
methods, errorReporter(), processingEnv, executable, builtType, autoBuilderType);
methods,
errorReporter(),
processingEnv,
executable,
builtType,
autoBuilderType,
propertyInitializers.keySet());
if (!maybeClassifier.isPresent() || errorReporter().errorCount() > 0) {
// We've already output one or more error messages.
return;
Expand All @@ -125,7 +192,7 @@ void processType(TypeElement autoBuilderType) {
Map<String, String> propertyToGetterName =
Maps.transformValues(classifier.builderGetters(), PropertyGetter::getName);
AutoBuilderTemplateVars vars = new AutoBuilderTemplateVars();
vars.props = propertySet(autoBuilderType, executable, propertyToGetterName);
vars.props = propertySet(executable, propertyToGetterName, propertyInitializers);
builder.defineVars(vars, classifier);
vars.identifiers = !processingEnv.getOptions().containsKey(OMIT_IDENTIFIERS_OPTION);
String generatedClassName = generatedClassName(autoBuilderType, "AutoBuilder_");
Expand All @@ -142,15 +209,9 @@ void processType(TypeElement autoBuilderType) {
}

private ImmutableSet<Property> propertySet(
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();
Map<String, String> propertyToGetterName,
ImmutableMap<String, String> builderInitializers) {
// 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 Down Expand Up @@ -188,11 +249,16 @@ private Property newProperty(
builderInitializer);
}

private ImmutableMap<String, String> autoAnnotationInitializers(
TypeElement autoBuilderType, ExecutableElement autoAnnotationMethod) {
private ImmutableMap<String, String> propertyInitializers(
TypeElement autoBuilderType, ExecutableElement executable) {
boolean autoAnnotation =
MoreElements.getAnnotationMirror(executable, AUTO_ANNOTATION_NAME).isPresent();
if (!autoAnnotation) {
return ImmutableMap.of();
}
// 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();
TypeMirror returnType = executable.getReturnType();
if (!returnType.getKind().equals(TypeKind.DECLARED)) {
return ImmutableMap.of();
}
Expand Down Expand Up @@ -452,4 +518,63 @@ Optional<String> nullableAnnotationForMethod(ExecutableElement propertyMethod) {
// TODO(b/183005059): implement
return Optional.empty();
}

private void buildAnnotation(
TypeElement autoBuilderType, TypeElement annotationType, String callMethod) {
if (!callMethod.isEmpty()) {
errorReporter()
.abortWithError(
autoBuilderType,
"[AutoBuilderAnnotationMethod] @AutoBuilder for an annotation must have an empty"
+ " callMethod, not \"%s\"",
callMethod);
}
String autoAnnotationClassName =
generatedClassName(autoBuilderType, AUTO_ANNOTATION_CLASS_PREFIX);
TypeElement autoAnnotationClass = elementUtils().getTypeElement(autoAnnotationClassName);
if (autoAnnotationClass != null) {
processType(autoBuilderType, autoAnnotationClass, "newAnnotation");
return;
}
AutoBuilderAnnotationTemplateVars vars = new AutoBuilderAnnotationTemplateVars();
vars.autoBuilderType = TypeEncoder.encode(autoBuilderType.asType());
vars.props = annotationBuilderPropertySet(annotationType);
vars.pkg = TypeSimplifier.packageNameOf(autoBuilderType);
vars.generated =
generatedAnnotation(elementUtils(), processingEnv.getSourceVersion())
.map(annotation -> TypeEncoder.encode(annotation.asType()))
.orElse("");
vars.className = TypeSimplifier.simpleNameOf(autoAnnotationClassName);
vars.annotationType = TypeEncoder.encode(annotationType.asType());
String text = vars.toText();
text = TypeEncoder.decode(text, processingEnv, vars.pkg, /* baseType= */ javaLangVoid);
text = Reformatter.fixup(text);
writeSourceFile(autoAnnotationClassName, text, autoBuilderType);
addDeferredType(autoBuilderType);
}

private ImmutableSet<Property> annotationBuilderPropertySet(TypeElement annotationType) {
// Translate the annotation elements into fake Property instances. We're really only interested
// in the name and type, so we can use them to declare a parameter of the generated
// @AutoAnnotation method. We'll generate a parameter for every element, even elements that
// don't have setters in the builder. The generated builder implementation will pass the default
// value from the annotation to those parameters.
return methodsIn(annotationType.getEnclosedElements()).stream()
.filter(m -> m.getParameters().isEmpty() && !m.getModifiers().contains(Modifier.STATIC))
.map(AutoBuilderProcessor::annotationBuilderProperty)
.collect(toImmutableSet());
}

private static Property annotationBuilderProperty(ExecutableElement annotationMethod) {
String name = annotationMethod.getSimpleName().toString();
TypeMirror type = annotationMethod.getReturnType();
return new Property(
name,
name,
TypeEncoder.encode(type),
type,
/* nullableAnnotation= */ Optional.empty(),
/* getter= */ "",
/* maybeBuilderInitializer= */ Optional.empty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ void processType(TypeElement type) {
.abortWithError(
type,
"[AutoValueImplAnnotation] @AutoValue may not be used to implement an annotation"
+ " interface; try using @AutoAnnotation instead");
+ " interface; try using @AutoAnnotation or @AutoBuilder instead");
}

// We are going to classify the methods of the @AutoValue class into several categories.
Expand Down
Loading

0 comments on commit 196c810

Please sign in to comment.