com.google.auto.value.processor.AutoValueProcessor Maven / Gradle / Ivy
Show all versions of auto-value Show documentation
/*
* Copyright (C) 2012 Google, Inc.
*
* 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.common.MoreElements.isAnnotationPresent;
import static com.google.common.collect.Sets.union;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.service.AutoService;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.AutoValueExtension;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.beans.Introspector;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
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.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.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
/**
* Javac annotation processor (compiler plugin) for value types; user code never references this
* class.
*
* @see AutoValue
* @see AutoValue User's Guide
* @author Éamonn McManus
*/
@AutoService(Processor.class)
public class AutoValueProcessor extends AbstractProcessor {
public AutoValueProcessor() {
this(AutoValueProcessor.class.getClassLoader());
}
@VisibleForTesting
AutoValueProcessor(ClassLoader loaderForExtensions) {
this.extensions = null;
this.loaderForExtensions = loaderForExtensions;
}
@VisibleForTesting
public AutoValueProcessor(Iterable extends AutoValueExtension> extensions) {
this.extensions = ImmutableList.copyOf(extensions);
this.loaderForExtensions = null;
}
@Override
public Set getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoValue.class.getName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* Used to test whether a fully-qualified name is AutoValue.class.getCanonicalName() or one of its
* nested annotations.
*/
private static final Pattern AUTO_VALUE_CLASSNAME_PATTERN =
Pattern.compile(Pattern.quote(AutoValue.class.getCanonicalName()) + "(\\..*)?");
private ErrorReporter errorReporter;
/**
* Qualified names of {@code @AutoValue} 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 deferredTypeNames = new ArrayList();
// Depending on how this AutoValueProcessor was constructed, we might already have a list of
// extensions when init() is run, or, if `extensions` is null, we have a ClassLoader that will be
// used to get the list using the ServiceLoader API.
private ImmutableList extensions;
private final ClassLoader loaderForExtensions;
private Types typeUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
errorReporter = new ErrorReporter(processingEnv);
typeUtils = processingEnv.getTypeUtils();
if (extensions == null) {
try {
extensions = ImmutableList.copyOf(
ServiceLoader.load(AutoValueExtension.class, loaderForExtensions));
// ServiceLoader.load returns a lazily-evaluated Iterable, so evaluate it eagerly now
// to discover any exceptions.
} catch (Throwable t) {
StringBuilder warning = new StringBuilder();
warning.append(
"An exception occurred while looking for AutoValue extensions. "
+ "No extensions will function.");
if (t instanceof ServiceConfigurationError) {
warning.append(" This may be due to a corrupt jar file in the compiler's classpath.");
}
warning.append(" Exception: ").append(t);
errorReporter.reportWarning(warning.toString(), null);
extensions = ImmutableList.of();
}
}
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
List deferredTypes = new ArrayList();
for (String deferred : deferredTypeNames) {
deferredTypes.add(processingEnv.getElementUtils().getTypeElement(deferred));
}
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("Did not generate @AutoValue class for " + type.getQualifiedName()
+ " because it references undefined types", type);
}
return false;
}
Collection extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(AutoValue.class);
List types = new ImmutableList.Builder()
.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) {
// Don't propagate this exception, which will confusingly crash the compiler.
// Instead, report a compiler error with the stack trace.
String trace = Throwables.getStackTraceAsString(e);
errorReporter.reportError("@AutoValue processor threw an exception: " + trace, type);
}
}
return false; // never claim annotation, because who knows what other processors want?
}
private 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 String generatedSubclassName(TypeElement type, int depth) {
return generatedClassName(type, Strings.repeat("$", depth) + "AutoValue_");
}
/**
* A property of an {@code @AutoValue} 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 annotations;
private final Optionalish optional;
Property(
String name,
String identifier,
ExecutableElement method,
String type,
TypeSimplifier typeSimplifier,
ImmutableSet excludedAnnotations) {
this.name = name;
this.identifier = identifier;
this.method = method;
this.type = type;
this.annotations = buildAnnotations(typeSimplifier, excludedAnnotations);
TypeMirror propertyType = method.getReturnType();
this.optional =
Optionalish.createIfOptional(propertyType, typeSimplifier.simplifyRaw(propertyType));
}
private ImmutableList buildAnnotations(
TypeSimplifier typeSimplifier, ImmutableSet excludedAnnotations) {
ImmutableList.Builder builder = ImmutableList.builder();
builder.addAll(copyAnnotations(method, typeSimplifier, excludedAnnotations));
for (AnnotationMirror annotationMirror :
Java8Support.getAnnotationMirrors(method.getReturnType())) {
AnnotationOutput annotationOutput = new AnnotationOutput(typeSimplifier);
builder.add(annotationOutput.sourceFormForAnnotation(annotationMirror));
}
return builder.build();
}
/**
* 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();
}
TypeElement getOwner() {
return (TypeElement) method.getEnclosingElement();
}
public TypeMirror getTypeMirror() {
return method.getReturnType();
}
public String getType() {
return type;
}
public TypeKind getKind() {
return method.getReturnType().getKind();
}
public List getAnnotations() {
return annotations;
}
/**
* 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 an 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 "@Nullable "} or {@code "@javax.annotation.Nullable "}.
*/
public String getNullableAnnotation() {
for (String annotationString : annotations) {
if (annotationString.equals("@Nullable") || annotationString.endsWith(".Nullable")) {
return annotationString + " ";
}
}
return "";
}
public boolean isNullable() {
return !getNullableAnnotation().isEmpty();
}
public String getAccess() {
return access(method);
}
@Override
public boolean equals(Object obj) {
return obj instanceof Property && ((Property) obj).method.equals(method);
}
@Override
public int hashCode() {
return method.hashCode();
}
}
/**
* A basic method on an @AutoValue class with no specific attached information, such as a {@code
* toBuilder()} method, or a {@code build()} method, where only the name and access type is needed
* in context.
*
* It implements JavaBean-style getters akin to {@link Property}.
*/
public static class SimpleMethod {
private final String access;
private final String name;
SimpleMethod(ExecutableElement method) {
this.access = access(method);
this.name = method.getSimpleName().toString();
}
public String getAccess() {
return access;
}
public String getName() {
return name;
}
}
static String access(ExecutableElement method) {
Set mods = method.getModifiers();
if (mods.contains(Modifier.PUBLIC)) {
return "public ";
} else if (mods.contains(Modifier.PROTECTED)) {
return "protected ";
} else {
return "";
}
}
private static boolean isJavaLangObject(TypeElement type) {
return type.getSuperclass().getKind() == TypeKind.NONE && type.getKind() == ElementKind.CLASS;
}
private enum ObjectMethodToOverride {
NONE, TO_STRING, EQUALS, HASH_CODE
}
private static ObjectMethodToOverride objectMethodToOverride(ExecutableElement method) {
String name = method.getSimpleName().toString();
switch (method.getParameters().size()) {
case 0:
if (name.equals("toString")) {
return ObjectMethodToOverride.TO_STRING;
} else if (name.equals("hashCode")) {
return ObjectMethodToOverride.HASH_CODE;
}
break;
case 1:
if (name.equals("equals")
&& method.getParameters().get(0).asType().toString().equals("java.lang.Object")) {
return ObjectMethodToOverride.EQUALS;
}
break;
default:
// No relevant Object methods have more than one parameter.
}
return ObjectMethodToOverride.NONE;
}
private void processType(TypeElement type) {
AutoValue autoValue = type.getAnnotation(AutoValue.class);
if (autoValue == null) {
// This shouldn't happen unless the compilation environment is buggy,
// but it has happened in the past and can crash the compiler.
errorReporter.abortWithError("annotation processor for @AutoValue was invoked with a type"
+ " that does not have that annotation; this is probably a compiler bug", type);
}
if (type.getKind() != ElementKind.CLASS) {
errorReporter.abortWithError(
"@" + AutoValue.class.getName() + " only applies to classes", type);
}
if (ancestorIsAutoValue(type)) {
errorReporter.abortWithError("One @AutoValue class may not extend another", type);
}
if (implementsAnnotation(type)) {
errorReporter.abortWithError("@AutoValue may not be used to implement an annotation"
+ " interface; try using @AutoAnnotation instead", type);
}
checkModifiersIfNested(type);
// We are going to classify the methods of the @AutoValue class into several categories.
// This covers the methods in the class itself and the ones it inherits from supertypes.
// First, the only concrete (non-abstract) methods we are interested in are overrides of
// Object methods (equals, hashCode, toString), which signal that we should not generate
// an implementation of those methods.
// Then, each abstract method is one of the following:
// (1) A property getter, like "abstract String foo()" or "abstract String getFoo()".
// (2) A toBuilder() method, which is any abstract no-arg method returning the Builder for
// this @AutoValue class.
// (3) An abstract method that will be consumed by an extension, such as
// Parcelable.describeContents() or Parcelable.writeToParcel(Parcel, int).
// The describeContents() example shows a quirk here: initially we will identify it as a
// property, which means that we need to reconstruct the list of properties after allowing
// extensions to consume abstract methods.
// If there are abstract methods that don't fit any of the categories above, that is an error
// which we signal explicitly to avoid confusion.
ImmutableSet methods = getLocalAndInheritedMethods(
type, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
ImmutableSet abstractMethods = abstractMethodsIn(methods);
BuilderSpec builderSpec = new BuilderSpec(type, processingEnv, errorReporter);
Optional builder = builderSpec.getBuilder();
ImmutableSet toBuilderMethods;
if (builder.isPresent()) {
toBuilderMethods = builder.get().toBuilderMethods(typeUtils, abstractMethods);
} else {
toBuilderMethods = ImmutableSet.of();
}
ImmutableSet propertyMethods =
propertyMethodsIn(immutableSetDifference(abstractMethods, toBuilderMethods));
ImmutableBiMap properties = propertyNameToMethodMap(propertyMethods);
ExtensionContext context =
new ExtensionContext(processingEnv, type, properties, abstractMethods);
ImmutableList applicableExtensions = applicableExtensions(type, context);
ImmutableSet consumedMethods = methodsConsumedByExtensions(
type, applicableExtensions, context, abstractMethods, properties);
if (!consumedMethods.isEmpty()) {
ImmutableSet allAbstractMethods = abstractMethods;
abstractMethods = immutableSetDifference(abstractMethods, consumedMethods);
toBuilderMethods = immutableSetDifference(toBuilderMethods, consumedMethods);
propertyMethods =
propertyMethodsIn(immutableSetDifference(abstractMethods, toBuilderMethods));
properties = propertyNameToMethodMap(propertyMethods);
context = new ExtensionContext(processingEnv, type, properties, allAbstractMethods);
}
boolean extensionsPresent = !applicableExtensions.isEmpty();
validateMethods(type, abstractMethods, toBuilderMethods, propertyMethods, extensionsPresent);
String finalSubclass = generatedSubclassName(type, 0);
AutoValueTemplateVars vars = new AutoValueTemplateVars();
vars.pkg = TypeSimplifier.packageNameOf(type);
vars.origClass = TypeSimplifier.classNameOf(type);
vars.simpleClassName = TypeSimplifier.simpleNameOf(vars.origClass);
vars.finalSubclass = TypeSimplifier.simpleNameOf(finalSubclass);
vars.types = processingEnv.getTypeUtils();
determineObjectMethodsToGenerate(methods, vars);
TypeSimplifier typeSimplifier =
defineVarsForType(type, vars, toBuilderMethods, propertyMethods, builder);
// Only copy annotations from a class if it has @AutoValue.CopyAnnotations.
if (isAnnotationPresent(type, AutoValue.CopyAnnotations.class)) {
Set excludedAnnotations =
union(
getFieldOfClasses(
type,
AutoValue.CopyAnnotations.class,
"exclude",
processingEnv.getElementUtils()),
getAnnotationsMarkedWithInherited(type));
vars.annotations = copyAnnotations(type, typeSimplifier, excludedAnnotations);
} else {
vars.annotations = ImmutableList.of();
}
GwtCompatibility gwtCompatibility = new GwtCompatibility(type);
vars.gwtCompatibleAnnotation = gwtCompatibility.gwtCompatibleAnnotationString();
int subclassDepth = writeExtensions(type, context, applicableExtensions);
String subclass = generatedSubclassName(type, subclassDepth);
vars.subclass = TypeSimplifier.simpleNameOf(subclass);
vars.isFinal = (subclassDepth == 0);
String text = vars.toText();
text = Reformatter.fixup(text);
writeSourceFile(subclass, text, type);
GwtSerialization gwtSerialization = new GwtSerialization(gwtCompatibility, processingEnv, type);
gwtSerialization.maybeWriteGwtSerializer(vars);
}
/** Implements the semantics of {@link AutoValue.CopyAnnotations}; see its javadoc. */
private static ImmutableList copyAnnotations(
Element type, TypeSimplifier typeSimplifier, Set excludedAnnotations) {
ImmutableList.Builder result = ImmutableList.builder();
AnnotationOutput annotationOutput = new AnnotationOutput(typeSimplifier);
for (AnnotationMirror annotation : type.getAnnotationMirrors()) {
String annotationFqName = getAnnotationFqName(annotation);
if (AUTO_VALUE_CLASSNAME_PATTERN.matcher(annotationFqName).matches()) {
// Skip AutoValue itself (and any of its nested annotations).
continue;
}
if (!excludedAnnotations.contains(annotationFqName)) {
result.add(annotationOutput.sourceFormForAnnotation(annotation));
}
}
return result.build();
}
/**
* Returns the fully-qualified name of an annotation-mirror, e.g.
* "com.google.auto.value.AutoValue".
*/
private static String getAnnotationFqName(AnnotationMirror annotation) {
return ((TypeElement) annotation.getAnnotationType().asElement()).getQualifiedName().toString();
}
/**
* Returns the contents of a {@code Class[]}-typed field in an annotation.
*
* This method is needed because directly reading the value of such a field from an
* AnnotationMirror throws:
* javax.lang.model.type.MirroredTypeException: Attempt to access Class object for TypeMirror Foo.
*
*
* @param element The element on which the annotation is present. e.g. the class being processed
* by AutoValue.
* @param annotation The class of the annotation to read from., e.g. {@link
* AutoValue.CopyAnnotations}.
* @param fieldName The name of the field to read, e.g. "exclude".
* @return a set of fully-qualified names of classes appearing in 'fieldName' on 'annotation' on
* 'element'.
*/
private ImmutableSet getFieldOfClasses(
Element element,
Class extends Annotation> annotation,
String fieldName,
Elements elementUtils) {
TypeElement annotationElement = elementUtils.getTypeElement(annotation.getCanonicalName());
if (annotationElement == null) {
// This can happen if the annotation is on the -processorpath but not on the -classpath.
return ImmutableSet.of();
}
TypeMirror annotationMirror = annotationElement.asType();
for (AnnotationMirror annot : element.getAnnotationMirrors()) {
if (!typeUtils.isSameType(annot.getAnnotationType(), annotationMirror)) {
continue;
}
for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> entry :
annot.getElementValues().entrySet()) {
if (fieldName.contentEquals(entry.getKey().getSimpleName())) {
ImmutableSet.Builder result = ImmutableSet.builder();
@SuppressWarnings("unchecked")
List annotationsToCopy =
(List) entry.getValue().getValue();
for (AnnotationValue annotationValue : annotationsToCopy) {
String qualifiedName =
((TypeElement) ((DeclaredType) annotationValue.getValue()).asElement())
.getQualifiedName()
.toString();
result.add(qualifiedName);
}
return result.build();
}
}
}
return ImmutableSet.of();
}
private ImmutableSet getAnnotationsMarkedWithInherited(Element element) {
ImmutableSet.Builder result = ImmutableSet.builder();
for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
if (isAnnotationPresent(annotation.getAnnotationType().asElement(), Inherited.class)) {
result.add(getAnnotationFqName(annotation));
}
}
return result.build();
}
// Invokes each of the given extensions to generate its subclass, and returns the number of
// hierarchy classes that extensions generated. This number is then the number of $ characters
// that should precede the name of the AutoValue implementation class.
// Assume the @AutoValue class is com.example.Foo.Bar. Then if there are no
// extensions the returned value will be 0, so the AutoValue implementation will be
// com.example.AutoValue_Foo_Bar. If there is one extension, it will be asked to
// generate AutoValue_Foo_Bar with parent $AutoValue_Foo_Bar. If it does so (returns
// non-null) then the returned value will be 1, so the AutoValue implementation will be
// com.example.$AutoValue_Foo_Bar. Otherwise, the returned value will still be 0. Likewise,
// if there is a second extension and both extensions return non-null, the first one will
// generate AutoValue_Foo_Bar with parent $AutoValue_Foo_Bar, the second will generate
// $AutoValue_Foo_Bar with parent $$AutoValue_Foo_Bar, and the returned value will be 2 for
// com.example.$$AutoValue_Foo_Bar.
private int writeExtensions(
TypeElement type,
ExtensionContext context,
ImmutableList applicableExtensions) {
int writtenSoFar = 0;
for (AutoValueExtension extension : applicableExtensions) {
String parentFqName = generatedSubclassName(type, writtenSoFar + 1);
String parentSimpleName = TypeSimplifier.simpleNameOf(parentFqName);
String classFqName = generatedSubclassName(type, writtenSoFar);
String classSimpleName = TypeSimplifier.simpleNameOf(classFqName);
boolean isFinal = (writtenSoFar == 0);
String source = extension.generateClass(context, classSimpleName, parentSimpleName, isFinal);
if (source != null) {
source = Reformatter.fixup(source);
writeSourceFile(classFqName, source, type);
writtenSoFar++;
}
}
return writtenSoFar;
}
private ImmutableList applicableExtensions(
TypeElement type, ExtensionContext context) {
List applicableExtensions = Lists.newArrayList();
List finalExtensions = Lists.newArrayList();
for (AutoValueExtension extension : extensions) {
if (extension.applicable(context)) {
if (extension.mustBeFinal(context)) {
finalExtensions.add(extension);
} else {
applicableExtensions.add(extension);
}
}
}
switch (finalExtensions.size()) {
case 0:
break;
case 1:
applicableExtensions.add(0, finalExtensions.get(0));
break;
default:
errorReporter.reportError(
"More than one extension wants to generate the final class: "
+ FluentIterable.from(finalExtensions).transform(ExtensionName.INSTANCE)
.join(Joiner.on(", ")),
type);
break;
}
return ImmutableList.copyOf(applicableExtensions);
}
private ImmutableSet methodsConsumedByExtensions(
TypeElement type,
ImmutableList applicableExtensions,
ExtensionContext context,
ImmutableSet abstractMethods,
ImmutableBiMap properties) {
Set consumed = Sets.newHashSet();
for (AutoValueExtension extension : applicableExtensions) {
Set consumedHere = Sets.newHashSet();
for (String consumedProperty : extension.consumeProperties(context)) {
ExecutableElement propertyMethod = properties.get(consumedProperty);
if (propertyMethod == null) {
errorReporter.reportError(
"Extension " + extensionName(extension)
+ " wants to consume a property that does not exist: " + consumedProperty,
type);
} else {
consumedHere.add(propertyMethod);
}
}
for (ExecutableElement consumedMethod : extension.consumeMethods(context)) {
if (!abstractMethods.contains(consumedMethod)) {
errorReporter.reportError(
"Extension " + extensionName(extension)
+ " wants to consume a method that is not one of the abstract methods in this"
+ " class: " + consumedMethod,
type);
} else {
consumedHere.add(consumedMethod);
}
}
for (ExecutableElement repeat : Sets.intersection(consumed, consumedHere)) {
errorReporter.reportError(
"Extension " + extensionName(extension) + " wants to consume a method that was already"
+ " consumed by another extension", repeat);
}
consumed.addAll(consumedHere);
}
return ImmutableSet.copyOf(consumed);
}
private void validateMethods(
TypeElement type,
ImmutableSet abstractMethods,
ImmutableSet toBuilderMethods,
ImmutableSet propertyMethods,
boolean extensionsPresent) {
boolean ok = true;
for (ExecutableElement method : abstractMethods) {
if (propertyMethods.contains(method)) {
ok &= checkReturnType(type, method);
} else if (!toBuilderMethods.contains(method)
&& objectMethodToOverride(method) == ObjectMethodToOverride.NONE) {
// This could reasonably be an error, were it not for an Eclipse bug in
// ElementUtils.override that sometimes fails to recognize that one method overrides
// another, and therefore leaves us with both an abstract method and the subclass method
// that overrides it. This shows up in AutoValueTest.LukesBase for example.
String message = "Abstract method is neither a property getter nor a Builder converter";
if (extensionsPresent) {
message += ", and no extension consumed it";
}
errorReporter.reportWarning(message, method);
}
}
if (!ok) {
throw new AbortProcessingException();
}
}
private static String extensionName(AutoValueExtension extension) {
return extension.getClass().getName();
}
private enum ExtensionName implements Function {
INSTANCE;
@Override public String apply(AutoValueExtension input) {
return extensionName(input);
}
}
private enum SimpleMethodFunction implements Function {
INSTANCE;
@Override
public SimpleMethod apply(ExecutableElement input) {
return new SimpleMethod(input);
}
}
private TypeSimplifier defineVarsForType(
TypeElement type,
AutoValueTemplateVars vars,
ImmutableSet toBuilderMethods,
ImmutableSet propertyMethods,
Optional builder) {
DeclaredType declaredType = MoreTypes.asDeclared(type.asType());
Set types = new TypeMirrorSet();
types.addAll(returnTypesOf(propertyMethods));
if (builder.isPresent()) {
types.addAll(builder.get().referencedTypes());
}
TypeElement generatedTypeElement =
processingEnv.getElementUtils().getTypeElement("javax.annotation.Generated");
if (generatedTypeElement != null) {
types.add(generatedTypeElement.asType());
}
TypeMirror javaUtilArrays = getTypeMirror(Arrays.class);
if (containsArrayType(types)) {
// If there are array properties then we will be referencing java.util.Arrays.
// Arrange to import it unless that would introduce ambiguity.
types.add(javaUtilArrays);
}
vars.toBuilderMethods =
FluentIterable.from(toBuilderMethods).transform(SimpleMethodFunction.INSTANCE).toList();
ImmutableSetMultimap excludedAnnotationsMap =
allMethodExcludedAnnotations(propertyMethods);
types.addAll(allMethodAnnotationTypes(propertyMethods, excludedAnnotationsMap));
String pkg = TypeSimplifier.packageNameOf(type);
TypeSimplifier typeSimplifier = new TypeSimplifier(typeUtils, pkg, types, declaredType);
vars.imports = typeSimplifier.typesToImport();
vars.generated = generatedTypeElement == null
? ""
: typeSimplifier.simplify(generatedTypeElement.asType());
vars.arrays = typeSimplifier.simplify(javaUtilArrays);
ImmutableBiMap methodToPropertyName =
propertyNameToMethodMap(propertyMethods).inverse();
Map methodToIdentifier = Maps.newLinkedHashMap(methodToPropertyName);
fixReservedIdentifiers(methodToIdentifier);
List props = new ArrayList();
EclipseHack eclipseHack = eclipseHack();
ImmutableMap returnTypes =
eclipseHack.methodReturnTypes(propertyMethods, declaredType);
for (ExecutableElement method : propertyMethods) {
TypeMirror returnType = returnTypes.get(method);
String propertyType = typeSimplifier.simplify(returnType);
String propertyName = methodToPropertyName.get(method);
String identifier = methodToIdentifier.get(method);
ImmutableSet excludedAnnotations =
ImmutableSet.builder()
.addAll(excludedAnnotationsMap.get(method))
.add(Override.class.getCanonicalName())
.build();
Property p =
new Property(
propertyName, identifier, method, propertyType, typeSimplifier, excludedAnnotations);
props.add(p);
if (p.isNullable() && returnType.getKind().isPrimitive()) {
errorReporter.reportError("Primitive types cannot be @Nullable", method);
}
}
// If we are running from Eclipse, undo the work of its compiler which sorts methods.
eclipseHack.reorderProperties(props);
vars.props = ImmutableSet.copyOf(props);
vars.serialVersionUID = getSerialVersionUID(type);
vars.formalTypes = typeSimplifier.formalTypeParametersString(type);
vars.actualTypes = TypeSimplifier.actualTypeParametersString(type);
vars.wildcardTypes = wildcardTypeParametersString(type);
// Check for @AutoValue.Builder and add appropriate variables if it is present.
if (builder.isPresent()) {
builder.get().defineVars(vars, typeSimplifier, methodToPropertyName);
}
return typeSimplifier;
}
private ImmutableSetMultimap allMethodExcludedAnnotations(
Iterable methods) {
ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder();
for (ExecutableElement method : methods) {
result.putAll(
method,
getFieldOfClasses(
method, AutoValue.CopyAnnotations.class, "exclude", processingEnv.getElementUtils()));
}
return result.build();
}
private ImmutableBiMap propertyNameToMethodMap(
Set propertyMethods) {
Map map = Maps.newLinkedHashMap();
boolean allPrefixed = gettersAllPrefixed(propertyMethods);
for (ExecutableElement method : propertyMethods) {
String methodName = method.getSimpleName().toString();
String name = allPrefixed ? nameWithoutPrefix(methodName) : methodName;
Object old = map.put(name, method);
if (old != null) {
errorReporter.reportError("More than one @AutoValue property called " + name, method);
}
}
return ImmutableBiMap.copyOf(map);
}
private static boolean gettersAllPrefixed(Set methods) {
return prefixedGettersIn(methods).size() == methods.size();
}
static ImmutableSet prefixedGettersIn(Iterable methods) {
ImmutableSet.Builder getters = ImmutableSet.builder();
for (ExecutableElement method : methods) {
String name = method.getSimpleName().toString();
// TODO(emcmanus): decide whether getfoo() (without a capital) is a getter. Currently it is.
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 all method annotations that should be imported in the generated class. Doesn't include
* AutoValue itself (and its nested annotations), any @Inherited annotations, and anything that's
* in excludedAnnotationsMap.
*/
private Set allMethodAnnotationTypes(
Iterable methods,
ImmutableSetMultimap excludedAnnotationsMap) {
Set annotationTypes = new TypeMirrorSet();
for (ExecutableElement method : methods) {
ImmutableSet excludedAnnotations = excludedAnnotationsMap.get(method);
for (AnnotationMirror annotationMirror : method.getAnnotationMirrors()) {
String annotationFqName = getAnnotationFqName(annotationMirror);
if (excludedAnnotations.contains(annotationFqName)) {
continue;
}
if (isAnnotationPresent(
annotationMirror.getAnnotationType().asElement(), Inherited.class)) {
continue;
}
if (AUTO_VALUE_CLASSNAME_PATTERN.matcher(annotationFqName).matches()) {
continue;
}
annotationTypes.add(annotationMirror.getAnnotationType());
}
for (AnnotationMirror annotationMirror :
Java8Support.getAnnotationMirrors(method.getReturnType())) {
annotationTypes.add(annotationMirror.getAnnotationType());
}
}
return annotationTypes;
}
/**
* 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
*
* rule is: the name of the property is the part after {@code get} or {@code is}, with the
* first letter lowercased unless 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 String nameWithoutPrefix(String name) {
if (name.startsWith("get")) {
name = name.substring(3);
} else {
assert name.startsWith("is");
name = name.substring(2);
}
return Introspector.decapitalize(name);
}
private void checkModifiersIfNested(TypeElement type) {
ElementKind enclosingKind = type.getEnclosingElement().getKind();
if (enclosingKind.isClass() || enclosingKind.isInterface()) {
if (type.getModifiers().contains(Modifier.PRIVATE)) {
errorReporter.abortWithError("@AutoValue class must not be private", type);
}
if (!type.getModifiers().contains(Modifier.STATIC)) {
errorReporter.abortWithError("Nested @AutoValue class must be static", type);
}
}
// 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.
}
// If we have a getter called getPackage() then we can't use the identifier "package" to represent
// its value since that's a reserved word.
private void fixReservedIdentifiers(Map methodToIdentifier) {
for (Map.Entry entry : methodToIdentifier.entrySet()) {
if (SourceVersion.isKeyword(entry.getValue())) {
entry.setValue(disambiguate(entry.getValue(), methodToIdentifier.values()));
}
}
}
private String disambiguate(String name, Collection existingNames) {
for (int i = 0; ; i++) {
String candidate = name + i;
if (!existingNames.contains(candidate)) {
return candidate;
}
}
}
private Set returnTypesOf(Iterable methods) {
Set returnTypes = new TypeMirrorSet();
for (ExecutableElement method : methods) {
returnTypes.add(method.getReturnType());
}
return returnTypes;
}
private static boolean containsArrayType(Set types) {
for (TypeMirror type : types) {
if (type.getKind() == TypeKind.ARRAY) {
return true;
}
}
return false;
}
/**
* Given a list of all methods defined in or inherited by a class, sets the equals, hashCode, and
* toString fields of vars according as the corresponding methods should be generated.
*/
private static void determineObjectMethodsToGenerate(
Set methods, AutoValueTemplateVars vars) {
// The defaults here only come into play when an ancestor class doesn't exist.
// Compilation will fail in that case, but we don't want it to crash the compiler with
// an exception before it does. If all ancestors do exist then we will definitely find
// definitions of these three methods (perhaps the ones in Object) so we will overwrite these:
vars.equals = false;
vars.hashCode = false;
vars.toString = false;
for (ExecutableElement method : methods) {
ObjectMethodToOverride override = objectMethodToOverride(method);
boolean canGenerate = method.getModifiers().contains(Modifier.ABSTRACT)
|| isJavaLangObject((TypeElement) method.getEnclosingElement());
switch (override) {
case EQUALS:
vars.equals = canGenerate;
break;
case HASH_CODE:
vars.hashCode = canGenerate;
break;
case TO_STRING:
vars.toString = canGenerate;
break;
default:
// Not a method from Object, nothing to do.
}
}
}
private ImmutableSet abstractMethodsIn(
ImmutableSet methods) {
Set noArgMethods = Sets.newHashSet();
ImmutableSet.Builder abstracts = ImmutableSet.builder();
for (ExecutableElement method : methods) {
if (method.getModifiers().contains(Modifier.ABSTRACT)) {
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();
}
private ImmutableSet propertyMethodsIn(
ImmutableSet abstractMethods) {
ImmutableSet.Builder properties = ImmutableSet.builder();
for (ExecutableElement method : abstractMethods) {
if (method.getParameters().isEmpty()
&& method.getReturnType().getKind() != TypeKind.VOID
&& objectMethodToOverride(method) == ObjectMethodToOverride.NONE) {
properties.add(method);
}
}
return properties.build();
}
private boolean 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);
return true;
} else {
errorReporter.reportError("An @AutoValue class cannot define an array-valued property"
+ " unless it is a primitive array", getter);
return false;
}
} else {
return true;
}
}
private void warnAboutPrimitiveArrays(TypeElement autoValueClass, ExecutableElement getter) {
SuppressWarnings suppressWarnings = getter.getAnnotation(SuppressWarnings.class);
if (suppressWarnings == null || !Arrays.asList(suppressWarnings.value()).contains("mutable")) {
// 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.
String warning =
"An @AutoValue property that is a primitive array returns the original array, "
+ "which can therefore be modified by the caller. If this 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.";
boolean sameClass = getter.getEnclosingElement().equals(autoValueClass);
if (sameClass) {
errorReporter.reportWarning(warning, getter);
} else {
errorReporter.reportWarning(
warning + " Method: " + getter.getEnclosingElement() + "." + getter, autoValueClass);
}
}
}
private void writeSourceFile(String className, String text, TypeElement originatingType) {
try {
JavaFileObject sourceFile =
processingEnv.getFiler().createSourceFile(className, originatingType);
Writer writer = sourceFile.openWriter();
try {
writer.write(text);
} finally {
writer.close();
}
} 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.
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"Could not write generated class " + className + ": " + e);
}
}
private boolean ancestorIsAutoValue(TypeElement type) {
while (true) {
TypeMirror parentMirror = type.getSuperclass();
if (parentMirror.getKind() == TypeKind.NONE) {
return false;
}
TypeElement parentElement = (TypeElement) typeUtils.asElement(parentMirror);
if (MoreElements.isAnnotationPresent(parentElement, AutoValue.class)) {
return true;
}
type = parentElement;
}
}
private boolean implementsAnnotation(TypeElement type) {
return typeUtils.isAssignable(type.asType(), getTypeMirror(Annotation.class));
}
// Return a string like "1234L" if type instanceof Serializable and defines
// serialVersionUID = 1234L, otherwise "".
private String getSerialVersionUID(TypeElement type) {
TypeMirror serializable = getTypeMirror(Serializable.class);
if (typeUtils.isAssignable(type.asType(), serializable)) {
List 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(
"serialVersionUID must be a static final long compile-time constant", field);
break;
}
}
}
}
return "";
}
private TypeMirror getTypeMirror(Class> c) {
return processingEnv.getElementUtils().getTypeElement(c.getName()).asType();
}
// The @AutoValue type, with a ? for every type.
// If we have @AutoValue abstract class Foo then this method will return
// just >.
private static String wildcardTypeParametersString(TypeElement type) {
List extends TypeParameterElement> typeParameters = type.getTypeParameters();
if (typeParameters.isEmpty()) {
return "";
} else {
return "<"
+ Joiner.on(", ").join(
FluentIterable.from(typeParameters).transform(Functions.constant("?")))
+ ">";
}
}
private static ImmutableSet immutableSetDifference(ImmutableSet a, ImmutableSet b) {
if (Collections.disjoint(a, b)) {
return a;
} else {
return ImmutableSet.copyOf(Sets.difference(a, b));
}
}
private EclipseHack eclipseHack() {
return new EclipseHack(processingEnv);
}
}