com.google.auto.value.processor.BuilderSpec Maven / Gradle / Ivy
Show all versions of auto-value Show documentation
/*
* Copyright 2014 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 static com.google.auto.common.MoreElements.getLocalAndInheritedMethods;
import static com.google.auto.value.processor.AutoValueOrOneOfProcessor.hasAnnotationMirror;
import static com.google.auto.value.processor.ClassNames.AUTO_VALUE_BUILDER_NAME;
import static com.google.common.collect.Sets.immutableEnumSet;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static javax.lang.model.util.ElementFilter.methodsIn;
import static javax.lang.model.util.ElementFilter.typesIn;
import com.google.auto.common.MoreTypes;
import com.google.auto.value.extension.AutoValueExtension;
import com.google.auto.value.processor.AutoValueOrOneOfProcessor.Property;
import com.google.auto.value.processor.PropertyBuilderClassifier.PropertyBuilder;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
/**
* Support for AutoValue builders.
*
* @author Éamonn McManus
*/
class BuilderSpec {
private final TypeElement autoValueClass;
private final ProcessingEnvironment processingEnv;
private final ErrorReporter errorReporter;
BuilderSpec(
TypeElement autoValueClass,
ProcessingEnvironment processingEnv,
ErrorReporter errorReporter) {
this.autoValueClass = autoValueClass;
this.processingEnv = processingEnv;
this.errorReporter = errorReporter;
}
private static final ImmutableSet CLASS_OR_INTERFACE =
immutableEnumSet(ElementKind.CLASS, ElementKind.INTERFACE);
/**
* Determines if the {@code @AutoValue} class for this instance has a correct nested
* {@code @AutoValue.Builder} class or interface and return a representation of it in an {@code
* Optional} if so.
*/
Optional getBuilder() {
Optional builderTypeElement = Optional.empty();
for (TypeElement containedClass : typesIn(autoValueClass.getEnclosedElements())) {
if (hasAnnotationMirror(containedClass, AUTO_VALUE_BUILDER_NAME)) {
if (!CLASS_OR_INTERFACE.contains(containedClass.getKind())) {
errorReporter.reportError(
"@AutoValue.Builder can only apply to a class or an interface", containedClass);
} else if (!containedClass.getModifiers().contains(Modifier.STATIC)) {
errorReporter.reportError(
"@AutoValue.Builder cannot be applied to a non-static class", containedClass);
} else if (builderTypeElement.isPresent()) {
errorReporter.reportError(
autoValueClass + " already has a Builder: " + builderTypeElement.get(),
containedClass);
} else {
builderTypeElement = Optional.of(containedClass);
}
}
}
if (builderTypeElement.isPresent()) {
return builderFrom(builderTypeElement.get());
} else {
return Optional.empty();
}
}
/** Representation of an {@code AutoValue.Builder} class or interface. */
class Builder implements AutoValueExtension.BuilderContext {
private final TypeElement builderTypeElement;
private ImmutableSet toBuilderMethods;
private ExecutableElement buildMethod;
private BuilderMethodClassifier classifier;
Builder(TypeElement builderTypeElement) {
this.builderTypeElement = builderTypeElement;
}
@Override
public TypeElement builderType() {
return builderTypeElement;
}
@Override
public Set builderMethods() {
return methodsIn(autoValueClass.getEnclosedElements()).stream()
.filter(
m ->
m.getParameters().isEmpty()
&& m.getModifiers().contains(Modifier.STATIC)
&& !m.getModifiers().contains(Modifier.PRIVATE)
&& erasedTypeIs(m.getReturnType(), builderTypeElement))
.collect(toSet());
}
@Override
public Optional buildMethod() {
return methodsIn(builderTypeElement.getEnclosedElements()).stream()
.filter(
m ->
m.getSimpleName().contentEquals("build")
&& !m.getModifiers().contains(Modifier.PRIVATE)
&& !m.getModifiers().contains(Modifier.STATIC)
&& m.getParameters().isEmpty()
&& erasedTypeIs(m.getReturnType(), autoValueClass))
.findFirst();
}
@Override
public ExecutableElement autoBuildMethod() {
return buildMethod;
}
@Override
public Map> setters() {
return Maps.transformValues(
classifier.propertyNameToSetters().asMap(),
propertySetters ->
propertySetters.stream().map(PropertySetter::getSetter).collect(toSet()));
}
@Override
public Map propertyBuilders() {
return Maps.transformValues(
classifier.propertyNameToPropertyBuilder(), PropertyBuilder::getPropertyBuilderMethod);
}
private boolean erasedTypeIs(TypeMirror type, TypeElement baseType) {
return type.getKind().equals(TypeKind.DECLARED)
&& MoreTypes.asDeclared(type).asElement().equals(baseType);
}
@Override
public Set toBuilderMethods() {
return toBuilderMethods;
}
/**
* Finds any methods in the set that return the builder type. If the builder has type parameters
* {@code }, then the return type of the method must be {@code Builder} with the
* same parameter names. We enforce elsewhere that the names and bounds of the builder
* parameters must be the same as those of the @AutoValue class. Here's a correct example:
*
*
* {@code @AutoValue abstract class Foo {
* abstract int someProperty();
*
* abstract Builder toBuilder();
*
* interface Builder {...}
* }}
*
*
* We currently impose that there cannot be more than one such method.
*/
ImmutableSet toBuilderMethods(
Types typeUtils, Set abstractMethods) {
List builderTypeParamNames =
builderTypeElement.getTypeParameters().stream()
.map(e -> e.getSimpleName().toString())
.collect(toList());
ImmutableSet.Builder methods = ImmutableSet.builder();
for (ExecutableElement method : abstractMethods) {
if (builderTypeElement.equals(typeUtils.asElement(method.getReturnType()))) {
methods.add(method);
DeclaredType returnType = MoreTypes.asDeclared(method.getReturnType());
List typeArguments =
returnType.getTypeArguments().stream()
.filter(t -> t.getKind().equals(TypeKind.TYPEVAR))
.map(t -> typeUtils.asElement(t).getSimpleName().toString())
.collect(toList());
if (!builderTypeParamNames.equals(typeArguments)) {
errorReporter.reportError(
"Builder converter method should return "
+ builderTypeElement
+ TypeSimplifier.actualTypeParametersString(builderTypeElement),
method);
}
}
}
ImmutableSet builderMethods = methods.build();
if (builderMethods.size() > 1) {
errorReporter.reportError(
"There can be at most one builder converter method", builderMethods.iterator().next());
}
this.toBuilderMethods = builderMethods;
return builderMethods;
}
void defineVars(
AutoValueTemplateVars vars,
ImmutableBiMap getterToPropertyName) {
Iterable builderMethods = abstractMethods(builderTypeElement);
boolean autoValueHasToBuilder = !toBuilderMethods.isEmpty();
ImmutableMap getterToPropertyType =
TypeVariables.rewriteReturnTypes(
processingEnv.getElementUtils(),
processingEnv.getTypeUtils(),
getterToPropertyName.keySet(),
autoValueClass,
builderTypeElement);
Optional optionalClassifier =
BuilderMethodClassifier.classify(
builderMethods,
errorReporter,
processingEnv,
autoValueClass,
builderTypeElement,
getterToPropertyName,
getterToPropertyType,
autoValueHasToBuilder);
if (!optionalClassifier.isPresent()) {
return;
}
this.classifier = optionalClassifier.get();
Set buildMethods = classifier.buildMethods();
if (buildMethods.size() != 1) {
Set extends Element> errorElements =
buildMethods.isEmpty() ? ImmutableSet.of(builderTypeElement) : buildMethods;
for (Element buildMethod : errorElements) {
errorReporter.reportError(
"Builder must have a single no-argument method returning "
+ autoValueClass
+ typeParamsString(),
buildMethod);
}
return;
}
this.buildMethod = Iterables.getOnlyElement(buildMethods);
vars.builderIsInterface = builderTypeElement.getKind() == ElementKind.INTERFACE;
vars.builderTypeName = TypeSimplifier.classNameOf(builderTypeElement);
vars.builderFormalTypes = TypeEncoder.formalTypeParametersString(builderTypeElement);
vars.builderActualTypes = TypeSimplifier.actualTypeParametersString(builderTypeElement);
vars.buildMethod = Optional.of(new SimpleMethod(buildMethod));
vars.builderGetters = classifier.builderGetters();
vars.builderSetters = classifier.propertyNameToSetters();
vars.builderPropertyBuilders =
ImmutableMap.copyOf(classifier.propertyNameToPropertyBuilder());
Set required = new LinkedHashSet<>(vars.props);
for (Property property : vars.props) {
if (property.isNullable()
|| property.getOptional() != null
|| vars.builderPropertyBuilders.containsKey(property.getName())) {
required.remove(property);
}
}
vars.builderRequiredProperties = ImmutableSet.copyOf(required);
}
}
/**
* Information about a builder property getter, referenced from the autovalue.vm template. A
* property called foo (defined by a method {@code T foo()} or {@code T getFoo()}) can have a
* getter method in the builder with the same name ({@code foo()} or {@code getFoo()}) and a
* return type of either {@code T} or {@code Optional}. The {@code Optional} form can be
* used to tell whether the property has been set. Here, {@code Optional} can be either {@code
* java.util.Optional} or {@code com.google.common.base.Optional}. If {@code T} is {@code int},
* {@code long}, or {@code double}, then instead of {@code Optional} we can have {@code
* OptionalInt} etc. If {@code T} is a primitive type (including these ones but also the other
* five) then {@code Optional} can be the corresponding boxed type.
*/
public static class PropertyGetter {
private final String access;
private final String type;
private final Optionalish optional;
/**
* Makes a new {@code PropertyGetter} instance.
*
* @param method the source method which this getter is implementing.
* @param type the type that the getter returns. This is written to take imports into account,
* so it might be {@code List} for example. It is either identical to the type of
* the corresponding getter in the {@code @AutoValue} class, or it is an optional wrapper,
* like {@code Optional>}.
* @param optional a representation of the {@code Optional} type that the getter returns, if
* this is an optional getter, or null otherwise. An optional getter is one that returns
* {@code Optional} rather than {@code T}, as explained above.
*/
PropertyGetter(ExecutableElement method, String type, Optionalish optional) {
this.access = SimpleMethod.access(method);
this.type = type;
this.optional = optional;
}
public String getAccess() {
return access;
}
public String getType() {
return type;
}
public Optionalish getOptional() {
return optional;
}
}
/**
* Information about a property setter, referenced from the autovalue.vm template. A property
* called foo (defined by a method {@code T foo()} or {@code T getFoo()}) can have a setter method
* {@code foo(T)} or {@code setFoo(T)} that returns the builder type. Additionally, it can have a
* setter with a type that can be copied to {@code T} through a {@code copyOf} method; for example
* a property {@code foo} of type {@code ImmutableSet} can be set with a method {@code
* setFoo(Collection foos)}. And, if {@code T} is {@code Optional}, it can have a setter
* with a type that can be copied to {@code T} through {@code Optional.of}.
*/
public static class PropertySetter {
private final ExecutableElement setter;
private final String access;
private final String name;
private final String parameterTypeString;
private final boolean primitiveParameter;
private final String nullableAnnotation;
private final Function copyFunction;
PropertySetter(
ExecutableElement setter, TypeMirror parameterType, Function copyFunction) {
this.setter = setter;
this.copyFunction = copyFunction;
this.access = SimpleMethod.access(setter);
this.name = setter.getSimpleName().toString();
primitiveParameter = parameterType.getKind().isPrimitive();
this.parameterTypeString = parameterTypeString(setter, parameterType);
VariableElement parameterElement = Iterables.getOnlyElement(setter.getParameters());
Optional maybeNullable =
AutoValueOrOneOfProcessor.nullableAnnotationFor(parameterElement, parameterType);
this.nullableAnnotation = maybeNullable.orElse("");
}
ExecutableElement getSetter() {
return setter;
}
private static String parameterTypeString(ExecutableElement setter, TypeMirror parameterType) {
if (setter.isVarArgs()) {
TypeMirror componentType = MoreTypes.asArray(parameterType).getComponentType();
// This is a bit ugly. It's OK to annotate just the component type, because if it is
// say `@Nullable String` then we will end up with `@Nullable String...`. Unlike the
// normal array case, we can't have the situation where the array itself is annotated;
// you can write `String @Nullable []` to mean that, but you can't write
// `String @Nullable ...`.
return TypeEncoder.encodeWithAnnotations(componentType) + "...";
} else {
return TypeEncoder.encodeWithAnnotations(parameterType);
}
}
public String getAccess() {
return access;
}
public String getName() {
return name;
}
public String getParameterType() {
return parameterTypeString;
}
public boolean getPrimitiveParameter() {
return primitiveParameter;
}
public String getNullableAnnotation() {
return nullableAnnotation;
}
public String copy(AutoValueProcessor.Property property) {
String copy = copyFunction.apply(property.toString());
// Add a null guard only in cases where we are using copyOf and the property is @Nullable.
if (property.isNullable() && !copy.equals(property.toString())) {
copy = String.format("(%s == null ? null : %s)", property, copy);
}
return copy;
}
}
/**
* Returns a representation of the given {@code @AutoValue.Builder} class or interface. If the
* class or interface has abstract methods that could not be part of any builder, emits error
* messages and returns Optional.empty().
*/
private Optional builderFrom(TypeElement builderTypeElement) {
// We require the builder to have the same type parameters as the @AutoValue class, meaning the
// same names and bounds. In principle the type parameters could have different names, but that
// would be confusing, and our code would reject it anyway because it wouldn't consider that
// the return type of Foo build() was really the same as the declaration of Foo. This
// check produces a better error message in that case and similar ones.
if (!sameTypeParameters(autoValueClass, builderTypeElement)) {
errorReporter.reportError(
"Type parameters of "
+ builderTypeElement
+ " must have same names and bounds as "
+ "type parameters of "
+ autoValueClass,
builderTypeElement);
return Optional.empty();
}
return Optional.of(new Builder(builderTypeElement));
}
private static boolean sameTypeParameters(TypeElement a, TypeElement b) {
int nTypeParameters = a.getTypeParameters().size();
if (nTypeParameters != b.getTypeParameters().size()) {
return false;
}
for (int i = 0; i < nTypeParameters; i++) {
TypeParameterElement aParam = a.getTypeParameters().get(i);
TypeParameterElement bParam = b.getTypeParameters().get(i);
if (!aParam.getSimpleName().equals(bParam.getSimpleName())) {
return false;
}
Set autoValueBounds = new TypeMirrorSet(aParam.getBounds());
Set builderBounds = new TypeMirrorSet(bParam.getBounds());
if (!autoValueBounds.equals(builderBounds)) {
return false;
}
}
return true;
}
// Return a set of all abstract methods in the given TypeElement or inherited from ancestors.
private Set abstractMethods(TypeElement typeElement) {
Set methods =
getLocalAndInheritedMethods(
typeElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
ImmutableSet.Builder abstractMethods = ImmutableSet.builder();
for (ExecutableElement method : methods) {
if (method.getModifiers().contains(Modifier.ABSTRACT)) {
abstractMethods.add(method);
}
}
return abstractMethods.build();
}
private String typeParamsString() {
return TypeSimplifier.actualTypeParametersString(autoValueClass);
}
}