blob: 4214c128ed8437d842573e63c70dd0b632df0c4b [file] [log] [blame]
/*
* Copyright 2018 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.AnnotationMirrors.getAnnotationValue;
import static com.google.auto.common.GeneratedAnnotations.generatedAnnotation;
import static com.google.auto.common.MoreElements.getPackage;
import static com.google.auto.common.MoreElements.isAnnotationPresent;
import static com.google.auto.value.processor.ClassNames.AUTO_VALUE_PACKAGE_NAME;
import static com.google.auto.value.processor.ClassNames.COPY_ANNOTATIONS_NAME;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.Sets.union;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.common.Visibility;
import com.google.auto.value.processor.MissingTypes.MissingTypeException;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Predicate;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
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.Name;
import javax.lang.model.element.QualifiedNameable;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleAnnotationValueVisitor8;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
/**
* Shared code between AutoValueProcessor and AutoOneOfProcessor.
*
* @author emcmanus@google.com (Éamonn McManus)
*/
abstract class AutoValueOrOneOfProcessor extends AbstractProcessor {
private final String annotationClassName;
/**
* Qualified names of {@code @AutoValue} or {@code AutoOneOf} classes that we attempted to process
* but had to abandon because we needed other types that they referenced and those other types
* were missing.
*/
private final List<String> deferredTypeNames = new ArrayList<>();
AutoValueOrOneOfProcessor(String annotationClassName) {
this.annotationClassName = annotationClassName;
}
/** The annotation we are processing, {@code AutoValue} or {@code AutoOneOf}. */
private TypeElement annotationType;
/** The simple name of {@link #annotationType}. */
private String simpleAnnotationName;
private ErrorReporter errorReporter;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
errorReporter = new ErrorReporter(processingEnv);
}
final ErrorReporter errorReporter() {
return errorReporter;
}
final Types typeUtils() {
return processingEnv.getTypeUtils();
}
final Elements elementUtils() {
return processingEnv.getElementUtils();
}
/**
* Qualified names of {@code @AutoValue} or {@code AutoOneOf} classes that we attempted to process
* but had to abandon because we needed other types that they referenced and those other types
* were missing. This is used by tests.
*/
final ImmutableList<String> deferredTypeNames() {
return ImmutableList.copyOf(deferredTypeNames);
}
@Override
public final SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* A property of an {@code @AutoValue} or {@code @AutoOneOf} class, defined by one of its abstract
* methods. An instance of this class is made available to the Velocity template engine for each
* property. The public methods of this class define JavaBeans-style properties that are
* accessible from templates. For example {@link #getType()} means we can write {@code $p.type}
* for a Velocity variable {@code $p} that is a {@code Property}.
*/
public static class Property {
private final String name;
private final String identifier;
private final ExecutableElement method;
private final String type;
private final ImmutableList<String> fieldAnnotations;
private final ImmutableList<String> methodAnnotations;
private final Optional<String> nullableAnnotation;
private final Optionalish optional;
Property(
String name,
String identifier,
ExecutableElement method,
String type,
ImmutableList<String> fieldAnnotations,
ImmutableList<String> methodAnnotations,
Optional<String> nullableAnnotation) {
this.name = name;
this.identifier = identifier;
this.method = method;
this.type = type;
this.fieldAnnotations = fieldAnnotations;
this.methodAnnotations = methodAnnotations;
this.nullableAnnotation = nullableAnnotation;
TypeMirror propertyType = method.getReturnType();
this.optional = Optionalish.createIfOptional(propertyType);
}
/**
* 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}.
* If it was {@code getFoo()} then it will be {@code foo}. If it was {@code getPackage()} then
* it will be something like {@code package0}, since {@code package} is a reserved word.
*/
@Override
public String toString() {
return identifier;
}
/**
* Returns the name of the property as it should be used in strings visible to users. This is
* usually the same as {@code toString()}, except that if we had to use an identifier like
* "package0" because "package" is a reserved word, the name here will be the original
* "package".
*/
public String getName() {
return name;
}
/**
* Returns the name of the getter method for this property as defined by the {@code @AutoValue}
* class. For property {@code foo}, this will be {@code foo} or {@code getFoo} or {@code isFoo}.
*/
public String getGetter() {
return method.getSimpleName().toString();
}
public TypeMirror getTypeMirror() {
return method.getReturnType();
}
public String getType() {
return type;
}
public TypeKind getKind() {
return method.getReturnType().getKind();
}
/**
* Returns the annotations (in string form) that should be applied to the property's field
* declaration.
*/
public List<String> getFieldAnnotations() {
return fieldAnnotations;
}
/**
* Returns the annotations (in string form) that should be applied to the property's method
* implementation.
*/
public List<String> getMethodAnnotations() {
return methodAnnotations;
}
/**
* Returns an {@link Optionalish} representing the kind of Optional that this property's type
* is, or null if the type is not an Optional of any kind.
*/
public Optionalish getOptional() {
return optional;
}
/**
* 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
* string with a trailing space, such as {@code "@`javax.annotation.Nullable` "}, where the
* {@code ``} is the encoding used by {@link TypeEncoder}. If the property is nullable by virtue
* of its <i>type</i> rather than its method being {@code @Nullable}, this method returns the
* empty string, because the {@code @Nullable} will appear when the type is spelled out. In this
* case, {@link #nullableAnnotation} is present but empty.
*/
public final String getNullableAnnotation() {
return nullableAnnotation.orElse("");
}
public boolean isNullable() {
return nullableAnnotation.isPresent();
}
public String getAccess() {
return SimpleMethod.access(method);
}
@Override
public boolean equals(Object obj) {
return obj instanceof Property && ((Property) obj).method.equals(method);
}
@Override
public int hashCode() {
return method.hashCode();
}
}
@Override
public final boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotationType = elementUtils().getTypeElement(annotationClassName);
if (annotationType == null) {
// This should not happen. If the annotation type is not found, how did the processor get
// triggered?
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"Did not process @"
+ annotationClassName
+ " because the annotation class was not found");
return false;
}
simpleAnnotationName = annotationType.getSimpleName().toString();
List<TypeElement> deferredTypes =
deferredTypeNames
.stream()
.map(name -> elementUtils().getTypeElement(name))
.collect(toList());
if (roundEnv.processingOver()) {
// This means that the previous round didn't generate any new sources, so we can't have found
// any new instances of @AutoValue; and we can't have any new types that are the reason a type
// was in deferredTypes.
for (TypeElement type : deferredTypes) {
errorReporter.reportError(
type,
"Did not generate @%s class for %s because it references undefined types",
simpleAnnotationName,
type.getQualifiedName());
}
return false;
}
Collection<? extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(annotationType);
List<TypeElement> types =
new ImmutableList.Builder<TypeElement>()
.addAll(deferredTypes)
.addAll(ElementFilter.typesIn(annotatedElements))
.build();
deferredTypeNames.clear();
for (TypeElement type : types) {
try {
processType(type);
} catch (AbortProcessingException e) {
// We abandoned this type; continue with the next.
} catch (MissingTypeException e) {
// We abandoned this type, but only because we needed another type that it references and
// that other type was missing. It is possible that the missing type will be generated by
// further annotation processing, so we will try again on the next round (perhaps failing
// again and adding it back to the list). We save the name of the @AutoValue type rather
// than its TypeElement because it is not guaranteed that it will be represented by
// the same TypeElement on the next round.
deferredTypeNames.add(type.getQualifiedName().toString());
} catch (RuntimeException e) {
String trace = Throwables.getStackTraceAsString(e);
errorReporter.reportError(
type, "@%s processor threw an exception: %s", simpleAnnotationName, trace);
throw e;
}
}
return false; // never claim annotation, because who knows what other processors want?
}
/**
* Analyzes a single {@code @AutoValue} or {@code @AutoOneOf} class, and outputs the corresponding
* implementation class or classes.
*
* @param type the class with the {@code @AutoValue} or {@code @AutoOneOf} annotation.
*/
abstract void processType(TypeElement type);
/**
* Returns the appropriate {@code @Nullable} annotation to put on the implementation of the given
* property method, and indicates whether the property is in fact nullable. The annotation in
* question is on the method, not its return type. If instead the return type is
* {@code @Nullable}, this method returns {@code Optional.of("")}, to indicate that the property
* is nullable but the <i>method</i> isn't. The {@code @Nullable} annotation will instead appear
* when the return type of the method is spelled out in the implementation.
*/
abstract Optional<String> nullableAnnotationForMethod(ExecutableElement propertyMethod);
/**
* Returns the ordered set of {@link Property} definitions for the given {@code @AutoValue} or
* {@code AutoOneOf} type.
*
* @param annotatedPropertyMethods a map from property methods to the method annotations that
* should go on the implementation of those methods. These annotations are method annotations
* specifically. Type annotations do not appear because they are considered part of the return
* type and will appear when that is spelled out. Annotations that are excluded by {@code
* AutoValue.CopyAnnotations} also do not appear here.
*/
final ImmutableSet<Property> propertySet(
ImmutableMap<ExecutableElement, TypeMirror> propertyMethodsAndTypes,
ImmutableListMultimap<ExecutableElement, AnnotationMirror> annotatedPropertyFields,
ImmutableListMultimap<ExecutableElement, AnnotationMirror> annotatedPropertyMethods) {
ImmutableBiMap<ExecutableElement, String> methodToPropertyName =
propertyNameToMethodMap(propertyMethodsAndTypes.keySet()).inverse();
Map<ExecutableElement, String> methodToIdentifier = new LinkedHashMap<>(methodToPropertyName);
fixReservedIdentifiers(methodToIdentifier);
ImmutableSet.Builder<Property> props = ImmutableSet.builder();
propertyMethodsAndTypes.forEach(
(propertyMethod, returnType) -> {
String propertyType = TypeEncoder.encodeWithAnnotations(
returnType, getExcludedAnnotationTypes(propertyMethod));
String propertyName = methodToPropertyName.get(propertyMethod);
String identifier = methodToIdentifier.get(propertyMethod);
ImmutableList<String> fieldAnnotations =
annotationStrings(annotatedPropertyFields.get(propertyMethod));
ImmutableList<AnnotationMirror> methodAnnotationMirrors =
annotatedPropertyMethods.get(propertyMethod);
ImmutableList<String> methodAnnotations = annotationStrings(methodAnnotationMirrors);
Optional<String> nullableAnnotation = nullableAnnotationForMethod(propertyMethod);
Property p =
new Property(
propertyName,
identifier,
propertyMethod,
propertyType,
fieldAnnotations,
methodAnnotations,
nullableAnnotation);
props.add(p);
if (p.isNullable() && returnType.getKind().isPrimitive()) {
errorReporter().reportError(propertyMethod, "Primitive types cannot be @Nullable");
}
});
return props.build();
}
/** Defines the template variables that are shared by AutoValue and AutoOneOf. */
final void defineSharedVarsForType(
TypeElement type,
ImmutableSet<ExecutableElement> methods,
AutoValueOrOneOfTemplateVars vars) {
vars.pkg = TypeSimplifier.packageNameOf(type);
vars.origClass = TypeSimplifier.classNameOf(type);
vars.simpleClassName = TypeSimplifier.simpleNameOf(vars.origClass);
vars.generated =
generatedAnnotation(elementUtils(), processingEnv.getSourceVersion())
.map(annotation -> TypeEncoder.encode(annotation.asType()))
.orElse("");
vars.formalTypes = TypeEncoder.formalTypeParametersString(type);
vars.actualTypes = TypeSimplifier.actualTypeParametersString(type);
vars.wildcardTypes = wildcardTypeParametersString(type);
vars.annotations = copiedClassAnnotations(type);
Map<ObjectMethod, ExecutableElement> methodsToGenerate =
determineObjectMethodsToGenerate(methods);
vars.toString = methodsToGenerate.containsKey(ObjectMethod.TO_STRING);
vars.equals = methodsToGenerate.containsKey(ObjectMethod.EQUALS);
vars.hashCode = methodsToGenerate.containsKey(ObjectMethod.HASH_CODE);
vars.equalsParameterType = equalsParameterType(methodsToGenerate);
}
/** Returns the spelling to be used in the generated code for the given list of annotations. */
static ImmutableList<String> annotationStrings(List<? extends AnnotationMirror> annotations) {
// TODO(b/68008628): use ImmutableList.toImmutableList() when that works.
return ImmutableList.copyOf(
annotations.stream().map(AnnotationOutput::sourceFormForAnnotation).collect(toList()));
}
/**
* Returns the name of the generated {@code @AutoValue} or {@code @AutoOneOf} class, for example
* {@code AutoOneOf_TaskResult} or {@code $$AutoValue_SimpleMethod}.
*
* @param type the name of the type bearing the {@code @AutoValue} or {@code @AutoOneOf}
* annotation.
* @param prefix the prefix to use in the generated class. This may start with one or more dollar
* signs, for an {@code @AutoValue} implementation where there are AutoValue extensions.
*/
static String generatedClassName(TypeElement type, String prefix) {
String name = type.getSimpleName().toString();
while (type.getEnclosingElement() instanceof TypeElement) {
type = (TypeElement) type.getEnclosingElement();
name = type.getSimpleName() + "_" + name;
}
String pkg = TypeSimplifier.packageNameOf(type);
String dot = pkg.isEmpty() ? "" : ".";
return pkg + dot + prefix + name;
}
private static boolean isJavaLangObject(TypeElement type) {
return type.getSuperclass().getKind() == TypeKind.NONE && type.getKind() == ElementKind.CLASS;
}
enum ObjectMethod {
NONE,
TO_STRING,
EQUALS,
HASH_CODE
}
/**
* Determines which of the three public non-final methods from {@code java.lang.Object}, if any,
* is overridden by the given method.
*/
static ObjectMethod objectMethodToOverride(ExecutableElement method) {
String name = method.getSimpleName().toString();
switch (method.getParameters().size()) {
case 0:
if (name.equals("toString")) {
return ObjectMethod.TO_STRING;
} else if (name.equals("hashCode")) {
return ObjectMethod.HASH_CODE;
}
break;
case 1:
if (name.equals("equals")) {
TypeMirror param = getOnlyElement(method.getParameters()).asType();
if (param.getKind().equals(TypeKind.DECLARED)) {
TypeElement paramType = MoreTypes.asTypeElement(param);
if (paramType.getQualifiedName().contentEquals("java.lang.Object")) {
return ObjectMethod.EQUALS;
}
}
}
break;
default:
// No relevant Object methods have more than one parameter.
}
return ObjectMethod.NONE;
}
/** Returns a bi-map between property names and the corresponding abstract property methods. */
final ImmutableBiMap<String, ExecutableElement> propertyNameToMethodMap(
Set<ExecutableElement> propertyMethods) {
Map<String, ExecutableElement> map = new LinkedHashMap<>();
Set<String> reportedDups = new HashSet<>();
boolean allPrefixed = gettersAllPrefixed(propertyMethods);
for (ExecutableElement method : propertyMethods) {
String methodName = method.getSimpleName().toString();
String name = allPrefixed ? nameWithoutPrefix(methodName) : methodName;
ExecutableElement old = map.put(name, method);
if (old != null) {
List<ExecutableElement> contexts = new ArrayList<>(Arrays.asList(method));
if (reportedDups.add(name)) {
contexts.add(old);
}
// Report the error for both of the methods. If this is a third or further occurrence,
// reportedDups prevents us from reporting more than one error for the same method.
for (ExecutableElement context : contexts) {
errorReporter.reportError(
context, "More than one @%s property called %s", simpleAnnotationName, name);
}
}
}
return ImmutableBiMap.copyOf(map);
}
private static boolean gettersAllPrefixed(Set<ExecutableElement> methods) {
return prefixedGettersIn(methods).size() == methods.size();
}
/**
* Returns an appropriate annotation spelling to indicate the nullability of an element. If the
* return value is a non-empty Optional, that indicates that the element is nullable, and the
* string should be used to annotate it. If the return value is an empty Optional, the element is
* not nullable. The return value can be {@code Optional.of("")}, which indicates that the element
* is nullable but that the nullability comes from a type annotation. In this case, the annotation
* will appear when the type is written, and must not be specified again. If the Optional contains
* a present non-empty string then that string will end with a space.
*
* @param element the element that might be {@code @Nullable}, either a method or a parameter.
* @param elementType the relevant type of the element: the return type for a method, or the
* parameter type for a parameter.
*/
static Optional<String> nullableAnnotationFor(Element element, TypeMirror elementType) {
List<? extends AnnotationMirror> typeAnnotations = elementType.getAnnotationMirrors();
if (nullableAnnotationIndex(typeAnnotations).isPresent()) {
return Optional.of("");
}
List<? extends AnnotationMirror> elementAnnotations = element.getAnnotationMirrors();
OptionalInt nullableAnnotationIndex = nullableAnnotationIndex(elementAnnotations);
if (nullableAnnotationIndex.isPresent()) {
ImmutableList<String> annotations = annotationStrings(elementAnnotations);
return Optional.of(annotations.get(nullableAnnotationIndex.getAsInt()) + " ");
} else {
return Optional.empty();
}
}
private static OptionalInt nullableAnnotationIndex(List<? extends AnnotationMirror> annotations) {
for (int i = 0; i < annotations.size(); i++) {
if (isNullable(annotations.get(i))) {
return OptionalInt.of(i);
}
}
return OptionalInt.empty();
}
private static boolean isNullable(AnnotationMirror annotation) {
return annotation.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable");
}
/**
* Returns the subset of the given zero-arg methods whose names begin with {@code get}. Also
* includes {@code isFoo} methods if they return {@code boolean}. This corresponds to JavaBeans
* conventions.
*/
static ImmutableSet<ExecutableElement> prefixedGettersIn(Iterable<ExecutableElement> methods) {
ImmutableSet.Builder<ExecutableElement> getters = ImmutableSet.builder();
for (ExecutableElement method : methods) {
String name = method.getSimpleName().toString();
// Note that getfoo() (without a capital) is still a getter.
boolean get = name.startsWith("get") && !name.equals("get");
boolean is =
name.startsWith("is")
&& !name.equals("is")
&& method.getReturnType().getKind() == TypeKind.BOOLEAN;
if (get || is) {
getters.add(method);
}
}
return getters.build();
}
/**
* Returns the name of the property defined by the given getter. A getter called {@code getFoo()}
* or {@code isFoo()} defines a property called {@code foo}. For consistency with JavaBeans, a
* getter called {@code getHTMLPage()} defines a property called {@code HTMLPage}. The <a
* href="https://docs.oracle.com/javase/8/docs/api/java/beans/Introspector.html#decapitalize-java.lang.String-">
* rule</a> is: the name of the property is the part after {@code get} or {@code is}, with the
* first letter lowercased <i>unless</i> the first two letters are uppercase. This works well for
* the {@code HTMLPage} example, but in these more enlightened times we use {@code HtmlPage}
* anyway, so the special behaviour is not useful, and of course it behaves poorly with examples
* like {@code OAuth}.
*/
private static String nameWithoutPrefix(String name) {
if (name.startsWith("get")) {
name = name.substring(3);
} else {
assert name.startsWith("is");
name = name.substring(2);
}
return PropertyNames.decapitalizeLikeJavaBeans(name);
}
/**
* Checks that, if the given {@code @AutoValue} or {@code @AutoOneOf} class is nested, it is
* static and not private. This check is not necessary for correctness, since the generated code
* would not compile if the check fails, but it produces better error messages for the user.
*/
final void checkModifiersIfNested(TypeElement type) {
ElementKind enclosingKind = type.getEnclosingElement().getKind();
if (enclosingKind.isClass() || enclosingKind.isInterface()) {
if (type.getModifiers().contains(Modifier.PRIVATE)) {
errorReporter.abortWithError(
type, "@%s class must not be private", simpleAnnotationName);
} else if (Visibility.effectiveVisibilityOfElement(type).equals(Visibility.PRIVATE)) {
// The previous case, where the class itself is private, is much commoner so it deserves
// its own error message, even though it would be caught by the test here too.
errorReporter.abortWithError(
type, "@%s class must not be nested in a private class", simpleAnnotationName);
}
if (!type.getModifiers().contains(Modifier.STATIC)) {
errorReporter.abortWithError(
type, "Nested @%s class must be static", simpleAnnotationName);
}
}
// In principle type.getEnclosingElement() could be an ExecutableElement (for a class
// declared inside a method), but since RoundEnvironment.getElementsAnnotatedWith doesn't
// return such classes we won't see them here.
}
/**
* Modifies the values of the given map to avoid reserved words. If we have a getter called {@code
* getPackage()} then we can't use the identifier {@code package} to represent its value since
* that's a reserved word.
*/
static void fixReservedIdentifiers(Map<?, String> methodToIdentifier) {
for (Map.Entry<?, String> entry : methodToIdentifier.entrySet()) {
String name = entry.getValue();
if (SourceVersion.isKeyword(name) || !Character.isJavaIdentifierStart(name.codePointAt(0))) {
entry.setValue(disambiguate(name, methodToIdentifier.values()));
}
}
}
private static String disambiguate(String name, Collection<String> existingNames) {
if (!Character.isJavaIdentifierStart(name.codePointAt(0))) {
// You've defined a getter called get1st(). What were you thinking?
name = "_" + name;
if (!existingNames.contains(name)) {
return name;
}
}
for (int i = 0; ; i++) {
String candidate = name + i;
if (!existingNames.contains(candidate)) {
return candidate;
}
}
}
/**
* Given a list of all methods defined in or inherited by a class, returns a map indicating which
* of equals, hashCode, and toString should be generated. Each value in the map is the method that
* will be overridden by the generated method, which might be a method in {@code Object} or an
* abstract method in the {@code @AutoValue} class or an ancestor.
*/
private static Map<ObjectMethod, ExecutableElement> determineObjectMethodsToGenerate(
Set<ExecutableElement> methods) {
Map<ObjectMethod, ExecutableElement> methodsToGenerate = new EnumMap<>(ObjectMethod.class);
for (ExecutableElement method : methods) {
ObjectMethod override = objectMethodToOverride(method);
boolean canGenerate =
method.getModifiers().contains(Modifier.ABSTRACT)
|| isJavaLangObject((TypeElement) method.getEnclosingElement());
if (!override.equals(ObjectMethod.NONE) && canGenerate) {
methodsToGenerate.put(override, method);
}
}
return methodsToGenerate;
}
/**
* Returns the encoded parameter type of the {@code equals(Object)} method that is to be
* generated, or an empty string if the method is not being generated. The parameter type includes
* any type annotations, for example {@code @Nullable}.
*/
static String equalsParameterType(Map<ObjectMethod, ExecutableElement> methodsToGenerate) {
ExecutableElement equals = methodsToGenerate.get(ObjectMethod.EQUALS);
if (equals == null) {
return ""; // this will not be referenced because no equals method will be generated
}
TypeMirror parameterType = equals.getParameters().get(0).asType();
return TypeEncoder.encodeWithAnnotations(parameterType);
}
/**
* Returns the subset of all abstract methods in the given set of methods. A given method
* signature is only mentioned once, even if it is inherited on more than one path. If any of the
* abstract methods has a return type or parameter type that is not currently defined then this
* method will throw an exception that will cause us to defer processing of the current class
* until a later annotation-processing round.
*/
static ImmutableSet<ExecutableElement> abstractMethodsIn(Iterable<ExecutableElement> methods) {
Set<Name> noArgMethods = new HashSet<>();
ImmutableSet.Builder<ExecutableElement> abstracts = ImmutableSet.builder();
for (ExecutableElement method : methods) {
if (method.getModifiers().contains(Modifier.ABSTRACT)) {
MissingTypes.deferIfMissingTypesIn(method);
boolean hasArgs = !method.getParameters().isEmpty();
if (hasArgs || noArgMethods.add(method.getSimpleName())) {
// If an abstract method with the same signature is inherited on more than one path,
// we only add it once. At the moment we only do this check for no-arg methods. All
// methods that AutoValue will implement are either no-arg methods or equals(Object).
// The former is covered by this check and the latter will lead to vars.equals being
// set to true, regardless of how many times it appears. So the only case that is
// covered imperfectly here is that of a method that is inherited on more than one path
// and that will be consumed by an extension. We could check parameters as well, but that
// can be a bit tricky if any of the parameters are generic.
abstracts.add(method);
}
}
}
return abstracts.build();
}
/**
* Returns the subset of property methods in the given set of abstract methods, with their actual
* return types. A property method has no arguments, is not void, and is not {@code hashCode()} or
* {@code toString()}.
*/
ImmutableMap<ExecutableElement, TypeMirror> propertyMethodsIn(
Set<ExecutableElement> abstractMethods,
TypeElement autoValueOrOneOfType) {
DeclaredType declaredType = MoreTypes.asDeclared(autoValueOrOneOfType.asType());
ImmutableSet.Builder<ExecutableElement> properties = ImmutableSet.builder();
for (ExecutableElement method : abstractMethods) {
if (method.getParameters().isEmpty()
&& (method.getReturnType().getKind() != TypeKind.VOID || propertiesCanBeVoid())
&& objectMethodToOverride(method) == ObjectMethod.NONE) {
properties.add(method);
}
}
return new EclipseHack(processingEnv).methodReturnTypes(properties.build(), declaredType);
}
/** True if void properties are allowed. */
boolean propertiesCanBeVoid() {
return false;
}
/**
* Checks that the return type of the given property method is allowed. Currently, this means that
* it cannot be an array, unless it is a primitive array.
*/
final void checkReturnType(TypeElement autoValueClass, ExecutableElement getter) {
TypeMirror type = getter.getReturnType();
if (type.getKind() == TypeKind.ARRAY) {
TypeMirror componentType = ((ArrayType) type).getComponentType();
if (componentType.getKind().isPrimitive()) {
warnAboutPrimitiveArrays(autoValueClass, getter);
} else {
errorReporter.reportError(
getter,
"An @%s class cannot define an array-valued property unless it is a primitive array",
simpleAnnotationName);
}
}
}
private void warnAboutPrimitiveArrays(TypeElement autoValueClass, ExecutableElement getter) {
boolean suppressed = false;
Optional<AnnotationMirror> maybeAnnotation =
getAnnotationMirror(getter, "java.lang.SuppressWarnings");
if (maybeAnnotation.isPresent()) {
AnnotationValue listValue = getAnnotationValue(maybeAnnotation.get(), "value");
suppressed = listValue.accept(new ContainsMutableVisitor(), null);
}
if (!suppressed) {
// If the primitive-array property method is defined directly inside the @AutoValue class,
// then our error message should point directly to it. But if it is inherited, we don't
// want to try to make the error message point to the inherited definition, since that would
// be confusing (there is nothing wrong with the definition itself), and won't work if the
// inherited class is not being recompiled. Instead, in this case we point to the @AutoValue
// class itself, and we include extra text in the error message that shows the full name of
// the inherited method.
boolean sameClass = getter.getEnclosingElement().equals(autoValueClass);
Element element = sameClass ? getter : autoValueClass;
String context = sameClass ? "" : (" Method: " + getter.getEnclosingElement() + "." + getter);
errorReporter.reportWarning(
element,
"An @%s property that is a primitive array returns the original array, which can"
+ " therefore be modified by the caller. If this is OK, you can suppress this warning"
+ " with @SuppressWarnings(\"mutable\"). Otherwise, you should replace the property"
+ " with an immutable type, perhaps a simple wrapper around the original array.%s",
simpleAnnotationName,
context);
}
}
// Detects whether the visited AnnotationValue is an array that contains the string "mutable".
// The simpler approach using Element.getAnnotation(SuppressWarnings.class) doesn't work if
// the annotation has an undefined reference, like @SuppressWarnings(UNDEFINED).
// TODO(emcmanus): replace with a method from auto-common when that is available.
private static class ContainsMutableVisitor extends SimpleAnnotationValueVisitor8<Boolean, Void> {
@Override
public Boolean visitArray(List<? extends AnnotationValue> list, Void p) {
return list.stream().map(av -> av.getValue()).anyMatch("mutable"::equals);
}
}
/**
* Returns a string like {@code "1234L"} if {@code type instanceof Serializable} and defines
* {@code serialVersionUID = 1234L}; otherwise {@code ""}.
*/
final String getSerialVersionUID(TypeElement type) {
TypeMirror serializable = elementUtils().getTypeElement(Serializable.class.getName()).asType();
if (typeUtils().isAssignable(type.asType(), serializable)) {
List<VariableElement> fields = ElementFilter.fieldsIn(type.getEnclosedElements());
for (VariableElement field : fields) {
if (field.getSimpleName().contentEquals("serialVersionUID")) {
Object value = field.getConstantValue();
if (field.getModifiers().containsAll(Arrays.asList(Modifier.STATIC, Modifier.FINAL))
&& field.asType().getKind() == TypeKind.LONG
&& value != null) {
return value + "L";
} else {
errorReporter.reportError(
field, "serialVersionUID must be a static final long compile-time constant");
break;
}
}
}
}
return "";
}
/** Implements the semantics of {@code AutoValue.CopyAnnotations}; see its javadoc. */
ImmutableList<AnnotationMirror> annotationsToCopy(
Element autoValueType, Element typeOrMethod, Set<String> excludedAnnotations) {
ImmutableList.Builder<AnnotationMirror> result = ImmutableList.builder();
for (AnnotationMirror annotation : typeOrMethod.getAnnotationMirrors()) {
String annotationFqName = getAnnotationFqName(annotation);
// To be included, the annotation should not be in com.google.auto.value,
// and it should not be in the excludedAnnotations set.
if (!isInAutoValuePackage(annotationFqName)
&& !excludedAnnotations.contains(annotationFqName)
&& annotationVisibleFrom(annotation, autoValueType)) {
result.add(annotation);
}
}
return result.build();
}
/**
* True if the given class name is in the com.google.auto.value package or a subpackage. False if
* the class name contains {@code Test}, since many AutoValue tests under com.google.auto.value
* define their own annotations.
*/
private boolean isInAutoValuePackage(String className) {
return className.startsWith(AUTO_VALUE_PACKAGE_NAME) && !className.contains("Test");
}
ImmutableList<String> copiedClassAnnotations(TypeElement type) {
// Only copy annotations from a class if it has @AutoValue.CopyAnnotations.
if (hasAnnotationMirror(type, COPY_ANNOTATIONS_NAME)) {
Set<String> excludedAnnotations =
union(getExcludedAnnotationClassNames(type), getAnnotationsMarkedWithInherited(type));
return copyAnnotations(type, type, excludedAnnotations);
} else {
return ImmutableList.of();
}
}
/** Implements the semantics of {@code AutoValue.CopyAnnotations}; see its javadoc. */
private ImmutableList<String> copyAnnotations(
Element autoValueType, Element typeOrMethod, Set<String> excludedAnnotations) {
ImmutableList<AnnotationMirror> annotationsToCopy =
annotationsToCopy(autoValueType, typeOrMethod, excludedAnnotations);
return annotationStrings(annotationsToCopy);
}
/**
* Returns the contents of the {@code AutoValue.CopyAnnotations.exclude} element, as a set of
* {@code TypeMirror} where each type is an annotation type.
*/
private Set<TypeMirror> getExcludedAnnotationTypes(Element element) {
Optional<AnnotationMirror> maybeAnnotation =
getAnnotationMirror(element, COPY_ANNOTATIONS_NAME);
if (!maybeAnnotation.isPresent()) {
return ImmutableSet.of();
}
@SuppressWarnings("unchecked")
List<AnnotationValue> excludedClasses =
(List<AnnotationValue>) getAnnotationValue(maybeAnnotation.get(), "exclude").getValue();
return excludedClasses
.stream()
.map(annotationValue -> (DeclaredType) annotationValue.getValue())
.collect(toCollection(TypeMirrorSet::new));
}
/**
* Returns the contents of the {@code AutoValue.CopyAnnotations.exclude} element, as a set of
* strings that are fully-qualified class names.
*/
private Set<String> getExcludedAnnotationClassNames(Element element) {
return getExcludedAnnotationTypes(element)
.stream()
.map(MoreTypes::asTypeElement)
.map(typeElement -> typeElement.getQualifiedName().toString())
.collect(toSet());
}
private static Set<String> getAnnotationsMarkedWithInherited(Element element) {
return element
.getAnnotationMirrors()
.stream()
.filter(a -> isAnnotationPresent(a.getAnnotationType().asElement(), Inherited.class))
.map(a -> getAnnotationFqName(a))
.collect(toSet());
}
/**
* Returns the fully-qualified name of an annotation-mirror, e.g.
* "com.google.auto.value.AutoValue".
*/
private static String getAnnotationFqName(AnnotationMirror annotation) {
return ((QualifiedNameable) annotation.getAnnotationType().asElement())
.getQualifiedName()
.toString();
}
final ImmutableListMultimap<ExecutableElement, AnnotationMirror> propertyMethodAnnotationMap(
TypeElement type, ImmutableSet<ExecutableElement> propertyMethods) {
ImmutableListMultimap.Builder<ExecutableElement, AnnotationMirror> builder =
ImmutableListMultimap.builder();
for (ExecutableElement propertyMethod : propertyMethods) {
builder.putAll(propertyMethod, propertyMethodAnnotations(type, propertyMethod));
}
return builder.build();
}
private ImmutableList<AnnotationMirror> propertyMethodAnnotations(
TypeElement type, ExecutableElement method) {
ImmutableSet<String> excludedAnnotations =
ImmutableSet.<String>builder()
.addAll(getExcludedAnnotationClassNames(method))
.add(Override.class.getCanonicalName())
.build();
// We need to exclude type annotations from the ones being output on the method, since
// they will be output as part of the method's return type.
Set<String> returnTypeAnnotations = getReturnTypeAnnotations(method, a -> true);
Set<String> excluded = union(excludedAnnotations, returnTypeAnnotations);
return annotationsToCopy(type, method, excluded);
}
final ImmutableListMultimap<ExecutableElement, AnnotationMirror> propertyFieldAnnotationMap(
TypeElement type, ImmutableSet<ExecutableElement> propertyMethods) {
ImmutableListMultimap.Builder<ExecutableElement, AnnotationMirror> builder =
ImmutableListMultimap.builder();
for (ExecutableElement propertyMethod : propertyMethods) {
builder.putAll(propertyMethod, propertyFieldAnnotations(type, propertyMethod));
}
return builder.build();
}
private ImmutableList<AnnotationMirror> propertyFieldAnnotations(
TypeElement type, ExecutableElement method) {
if (!hasAnnotationMirror(method, COPY_ANNOTATIONS_NAME)) {
return ImmutableList.of();
}
ImmutableSet<String> excludedAnnotations =
ImmutableSet.<String>builder()
.addAll(getExcludedAnnotationClassNames(method))
.add(Override.class.getCanonicalName())
.build();
// We need to exclude type annotations from the ones being output on the method, since
// they will be output as part of the field's type.
Set<String> returnTypeAnnotations =
getReturnTypeAnnotations(method, this::annotationAppliesToFields);
Set<String> nonFieldAnnotations =
method
.getAnnotationMirrors()
.stream()
.map(a -> a.getAnnotationType().asElement())
.map(MoreElements::asType)
.filter(a -> !annotationAppliesToFields(a))
.map(e -> e.getQualifiedName().toString())
.collect(toSet());
Set<String> excluded =
ImmutableSet.<String>builder()
.addAll(excludedAnnotations)
.addAll(returnTypeAnnotations)
.addAll(nonFieldAnnotations)
.build();
return annotationsToCopy(type, method, excluded);
}
private Set<String> getReturnTypeAnnotations(
ExecutableElement method, Predicate<TypeElement> typeFilter) {
return method
.getReturnType()
.getAnnotationMirrors()
.stream()
.map(a -> a.getAnnotationType().asElement())
.map(MoreElements::asType)
.filter(typeFilter)
.map(e -> e.getQualifiedName().toString())
.collect(toSet());
}
private boolean annotationAppliesToFields(TypeElement annotation) {
Target target = annotation.getAnnotation(Target.class);
return target == null || Arrays.asList(target.value()).contains(ElementType.FIELD);
}
private boolean annotationVisibleFrom(AnnotationMirror annotation, Element from) {
Element annotationElement = annotation.getAnnotationType().asElement();
Visibility visibility = Visibility.effectiveVisibilityOfElement(annotationElement);
switch (visibility) {
case PUBLIC:
return true;
case PROTECTED:
// If the annotation is protected, it must be inside another class, call it C. If our
// @AutoValue class is Foo then, for the annotation to be visible, either Foo must be in the
// same package as C or Foo must be a subclass of C. If the annotation is visible from Foo
// then it is also visible from our generated subclass AutoValue_Foo.
// The protected case only applies to method annotations. An annotation on the AutoValue_Foo
// class itself can't be protected, even if AutoValue_Foo ultimately inherits from the
// class that defines the annotation. The JLS says "Access is permitted only within the
// body of a subclass":
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.6.2.1
// AutoValue_Foo is a top-level class, so an annotation on it cannot be in the body of a
// subclass of anything.
return getPackage(annotationElement).equals(getPackage(from))
|| typeUtils()
.isSubtype(from.asType(), annotationElement.getEnclosingElement().asType());
case DEFAULT:
return getPackage(annotationElement).equals(getPackage(from));
default:
return false;
}
}
/**
* Returns the {@code @AutoValue} or {@code @AutoOneOf} type parameters, with a ? for every type.
* If we have {@code @AutoValue abstract class Foo<T extends Something>} then this method will
* return just {@code <?>}.
*/
private static String wildcardTypeParametersString(TypeElement type) {
List<? extends TypeParameterElement> typeParameters = type.getTypeParameters();
if (typeParameters.isEmpty()) {
return "";
} else {
return typeParameters.stream().map(e -> "?").collect(joining(", ", "<", ">"));
}
}
// TODO(emcmanus,ronshapiro): move to auto-common
static Optional<AnnotationMirror> getAnnotationMirror(Element element, String annotationName) {
for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
TypeElement annotationElement = MoreTypes.asTypeElement(annotation.getAnnotationType());
if (annotationElement.getQualifiedName().contentEquals(annotationName)) {
return Optional.of(annotation);
}
}
return Optional.empty();
}
static boolean hasAnnotationMirror(Element element, String annotationName) {
return getAnnotationMirror(element, annotationName).isPresent();
}
final void writeSourceFile(String className, String text, TypeElement originatingType) {
try {
JavaFileObject sourceFile =
processingEnv.getFiler().createSourceFile(className, originatingType);
try (Writer writer = sourceFile.openWriter()) {
writer.write(text);
}
} catch (IOException e) {
// This should really be an error, but we make it a warning in the hope of resisting Eclipse
// bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=367599. If that bug manifests, we may get
// invoked more than once for the same file, so ignoring the ability to overwrite it is the
// right thing to do. If we are unable to write for some other reason, we should get a compile
// error later because user code will have a reference to the code we were supposed to
// generate (new AutoValue_Foo() or whatever) and that reference will be undefined.
errorReporter.reportWarning(
originatingType, "Could not write generated class %s: %s", className, e);
}
}
}