blob: d3cd9bd54b45f281a5a560fd06734fea3f3cb7f6 [file] [log] [blame]
/*
* 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.GeneratedAnnotations.generatedAnnotation;
import static com.google.auto.value.processor.ClassNames.AUTO_ANNOTATION_NAME;
import com.google.auto.common.MoreElements;
import com.google.auto.common.SuperficialValidation;
import com.google.auto.service.AutoService;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Primitives;
import com.google.errorprone.annotations.FormatMethod;
import java.io.IOException;
import java.io.Writer;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
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.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.WildcardType;
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;
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor;
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType;
/**
* Javac annotation processor (compiler plugin) to generate annotation implementations. User code
* never references this class.
*
* @author emcmanus@google.com (Éamonn McManus)
*/
@AutoService(Processor.class)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING)
@SupportedAnnotationTypes(AUTO_ANNOTATION_NAME)
public class AutoAnnotationProcessor extends AbstractProcessor {
public AutoAnnotationProcessor() {}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* Issue a compilation error. This method does not throw an exception, since we want to continue
* processing and perhaps report other errors.
*/
@FormatMethod
private void reportError(Element e, String msg, Object... msgParams) {
String formattedMessage = String.format(msg, msgParams);
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, formattedMessage, e);
}
/**
* Issue a compilation error and return an exception that, when thrown, will cause the processing
* of this class to be abandoned. This does not prevent the processing of other classes.
*/
@FormatMethod
private AbortProcessingException abortWithError(Element e, String msg, Object... msgParams) {
reportError(e, msg, msgParams);
return new AbortProcessingException();
}
private Elements elementUtils;
private Types typeUtils;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
elementUtils = processingEnv.getElementUtils();
typeUtils = processingEnv.getTypeUtils();
boolean claimed =
(annotations.size() == 1
&& annotations
.iterator()
.next()
.getQualifiedName()
.contentEquals(AUTO_ANNOTATION_NAME));
if (claimed) {
process(roundEnv);
return true;
} else {
return false;
}
}
private void process(RoundEnvironment roundEnv) {
TypeElement autoAnnotation = elementUtils.getTypeElement(AUTO_ANNOTATION_NAME);
Collection<? extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(autoAnnotation);
List<ExecutableElement> methods = ElementFilter.methodsIn(annotatedElements);
if (!SuperficialValidation.validateElements(methods) || methodsAreOverloaded(methods)) {
return;
}
for (ExecutableElement method : methods) {
try {
processMethod(method);
} catch (AbortProcessingException e) {
// We abandoned this type, but continue with the next.
} catch (RuntimeException e) {
String trace = Throwables.getStackTraceAsString(e);
reportError(method, "@AutoAnnotation processor threw an exception: %s", trace);
throw e;
}
}
}
private void processMethod(ExecutableElement method) {
if (!method.getModifiers().contains(Modifier.STATIC)) {
throw abortWithError(method, "@AutoAnnotation method must be static");
}
TypeElement annotationElement = getAnnotationReturnType(method);
Set<Class<?>> wrapperTypesUsedInCollections = wrapperTypesUsedInCollections(method);
ImmutableMap<String, ExecutableElement> memberMethods = getMemberMethods(annotationElement);
TypeElement methodClass = (TypeElement) method.getEnclosingElement();
String pkg = TypeSimplifier.packageNameOf(methodClass);
ImmutableMap<String, AnnotationValue> defaultValues = getDefaultValues(annotationElement);
ImmutableMap<String, Member> members = getMembers(method, memberMethods);
ImmutableMap<String, Parameter> parameters = getParameters(annotationElement, method, members);
validateParameters(annotationElement, method, members, parameters, defaultValues);
String generatedClassName = generatedClassName(method);
AutoAnnotationTemplateVars vars = new AutoAnnotationTemplateVars();
vars.annotationFullName = annotationElement.toString();
vars.annotationName = TypeEncoder.encode(annotationElement.asType());
vars.className = generatedClassName;
vars.generated = getGeneratedTypeName();
vars.members = members;
vars.params = parameters;
vars.pkg = pkg;
vars.wrapperTypesUsedInCollections = wrapperTypesUsedInCollections;
vars.gwtCompatible = isGwtCompatible(annotationElement);
ImmutableMap<String, Integer> invariableHashes = invariableHashes(members, parameters.keySet());
vars.invariableHashSum = 0;
for (int h : invariableHashes.values()) {
vars.invariableHashSum += h;
}
vars.invariableHashes = invariableHashes.keySet();
String text = vars.toText();
text = TypeEncoder.decode(text, processingEnv, pkg, annotationElement.asType());
text = Reformatter.fixup(text);
String fullName = fullyQualifiedName(pkg, generatedClassName);
writeSourceFile(fullName, text, methodClass);
}
private String getGeneratedTypeName() {
return generatedAnnotation(elementUtils, processingEnv.getSourceVersion())
.map(generatedAnnotation -> TypeEncoder.encode(generatedAnnotation.asType()))
.orElse("");
}
/**
* Returns the hashCode of the given AnnotationValue, if that hashCode is guaranteed to be always
* the same. The hashCode of a String or primitive type never changes. The hashCode of a Class or
* an enum constant does potentially change in different runs of the same program. The hashCode of
* an array doesn't change if the hashCodes of its elements don't. Although we could have a
* similar rule for nested annotation values, we currently don't.
*/
private static Optional<Integer> invariableHash(AnnotationValue annotationValue) {
Object value = annotationValue.getValue();
if (value instanceof String || Primitives.isWrapperType(value.getClass())) {
return Optional.of(value.hashCode());
} else if (value instanceof List<?>) {
@SuppressWarnings("unchecked") // by specification
List<? extends AnnotationValue> list = (List<? extends AnnotationValue>) value;
return invariableHash(list);
} else {
return Optional.empty();
}
}
private static Optional<Integer> invariableHash(
List<? extends AnnotationValue> annotationValues) {
int h = 1;
for (AnnotationValue annotationValue : annotationValues) {
Optional<Integer> maybeHash = invariableHash(annotationValue);
if (!maybeHash.isPresent()) {
return Optional.empty();
}
h = h * 31 + maybeHash.get();
}
return Optional.of(h);
}
/**
* Returns a map from the names of members with invariable hashCodes to the values of those
* hashCodes.
*/
private static ImmutableMap<String, Integer> invariableHashes(
ImmutableMap<String, Member> members, ImmutableSet<String> parameters) {
ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
for (String element : members.keySet()) {
if (!parameters.contains(element)) {
Member member = members.get(element);
AnnotationValue annotationValue = member.method.getDefaultValue();
Optional<Integer> invariableHash = invariableHash(annotationValue);
if (invariableHash.isPresent()) {
builder.put(element, (element.hashCode() * 127) ^ invariableHash.get());
}
}
}
return builder.build();
}
private boolean methodsAreOverloaded(List<ExecutableElement> methods) {
boolean overloaded = false;
Set<String> classNames = new HashSet<String>();
for (ExecutableElement method : methods) {
String qualifiedClassName =
fullyQualifiedName(
MoreElements.getPackage(method).getQualifiedName().toString(),
generatedClassName(method));
if (!classNames.add(qualifiedClassName)) {
overloaded = true;
reportError(method, "@AutoAnnotation methods cannot be overloaded");
}
}
return overloaded;
}
private String generatedClassName(ExecutableElement method) {
TypeElement type = (TypeElement) method.getEnclosingElement();
String name = type.getSimpleName().toString();
while (type.getEnclosingElement() instanceof TypeElement) {
type = (TypeElement) type.getEnclosingElement();
name = type.getSimpleName() + "_" + name;
}
return "AutoAnnotation_" + name + "_" + method.getSimpleName();
}
private TypeElement getAnnotationReturnType(ExecutableElement method) {
TypeMirror returnTypeMirror = method.getReturnType();
if (returnTypeMirror.getKind() == TypeKind.DECLARED) {
Element returnTypeElement = typeUtils.asElement(method.getReturnType());
if (returnTypeElement.getKind() == ElementKind.ANNOTATION_TYPE) {
return (TypeElement) returnTypeElement;
}
}
throw abortWithError(
method,
"Return type of @AutoAnnotation method must be an annotation type, not %s",
returnTypeMirror);
}
private ImmutableMap<String, ExecutableElement> getMemberMethods(TypeElement annotationElement) {
ImmutableMap.Builder<String, ExecutableElement> members = ImmutableMap.builder();
for (ExecutableElement member :
ElementFilter.methodsIn(annotationElement.getEnclosedElements())) {
String name = member.getSimpleName().toString();
members.put(name, member);
}
return members.build();
}
private ImmutableMap<String, Member> getMembers(
Element context, ImmutableMap<String, ExecutableElement> memberMethods) {
ImmutableMap.Builder<String, Member> members = ImmutableMap.builder();
for (Map.Entry<String, ExecutableElement> entry : memberMethods.entrySet()) {
ExecutableElement memberMethod = entry.getValue();
String name = memberMethod.getSimpleName().toString();
members.put(name, new Member(processingEnv, context, memberMethod));
}
return members.build();
}
private ImmutableMap<String, AnnotationValue> getDefaultValues(TypeElement annotationElement) {
ImmutableMap.Builder<String, AnnotationValue> defaultValues = ImmutableMap.builder();
for (ExecutableElement member :
ElementFilter.methodsIn(annotationElement.getEnclosedElements())) {
String name = member.getSimpleName().toString();
AnnotationValue defaultValue = member.getDefaultValue();
if (defaultValue != null) {
defaultValues.put(name, defaultValue);
}
}
return defaultValues.build();
}
private ImmutableMap<String, Parameter> getParameters(
TypeElement annotationElement, ExecutableElement method, Map<String, Member> members) {
ImmutableMap.Builder<String, Parameter> parameters = ImmutableMap.builder();
boolean error = false;
for (VariableElement parameter : method.getParameters()) {
String name = parameter.getSimpleName().toString();
Member member = members.get(name);
if (member == null) {
reportError(
parameter,
"@AutoAnnotation method parameter '%s' must have the same name as a member of %s",
name,
annotationElement);
error = true;
} else {
TypeMirror parameterType = parameter.asType();
TypeMirror memberType = member.getTypeMirror();
if (compatibleTypes(parameterType, memberType)) {
parameters.put(name, new Parameter(parameterType));
} else {
reportError(
parameter,
"@AutoAnnotation method parameter '%s' has type %s but %s.%s has type %s",
name,
parameterType,
annotationElement,
name,
memberType);
error = true;
}
}
}
if (error) {
throw new AbortProcessingException();
}
return parameters.build();
}
private void validateParameters(
TypeElement annotationElement,
ExecutableElement method,
ImmutableMap<String, Member> members,
ImmutableMap<String, Parameter> parameters,
ImmutableMap<String, AnnotationValue> defaultValues) {
boolean error = false;
for (String memberName : members.keySet()) {
if (!parameters.containsKey(memberName) && !defaultValues.containsKey(memberName)) {
reportError(
method,
"@AutoAnnotation method needs a parameter with name '%s' and type %s"
+ " corresponding to %s.%s, which has no default value",
memberName,
members.get(memberName).getType(),
annotationElement,
memberName);
error = true;
}
}
if (error) {
throw new AbortProcessingException();
}
}
/**
* Returns true if {@code parameterType} can be used to provide the value of an annotation member
* of type {@code memberType}. They must either be the same type, or the member type must be an
* array and the parameter type must be a collection of a compatible type.
*/
private boolean compatibleTypes(TypeMirror parameterType, TypeMirror memberType) {
if (typeUtils.isAssignable(parameterType, memberType)) {
// parameterType assignable to memberType, which in the restricted world of annotations
// means they are the same type, or maybe memberType is an annotation type and parameterType
// is a subtype of that annotation interface (why would you do that?).
return true;
}
// They're not the same, but we could still consider them compatible if for example
// parameterType is List<Integer> and memberType is int[]. We accept any type that is assignable
// to Collection<Integer> (in this example).
if (memberType.getKind() != TypeKind.ARRAY) {
return false;
}
TypeMirror arrayElementType = ((ArrayType) memberType).getComponentType();
TypeMirror wrappedArrayElementType =
arrayElementType.getKind().isPrimitive()
? typeUtils.boxedClass((PrimitiveType) arrayElementType).asType()
: arrayElementType;
TypeElement javaUtilCollection =
elementUtils.getTypeElement(Collection.class.getCanonicalName());
DeclaredType collectionOfElement =
typeUtils.getDeclaredType(javaUtilCollection, wrappedArrayElementType);
return typeUtils.isAssignable(parameterType, collectionOfElement);
}
/**
* Returns the wrapper types ({@code Integer.class} etc) that are used in collection parameters
* like {@code List<Integer>}. This is needed because we will emit a helper method for each such
* type, for example to convert {@code Collection<Integer>} into {@code int[]}.
*/
private Set<Class<?>> wrapperTypesUsedInCollections(ExecutableElement method) {
TypeElement javaUtilCollection = elementUtils.getTypeElement(Collection.class.getName());
ImmutableSet.Builder<Class<?>> usedInCollections = ImmutableSet.builder();
for (Class<?> wrapper : Primitives.allWrapperTypes()) {
DeclaredType collectionOfWrapper =
typeUtils.getDeclaredType(javaUtilCollection, getTypeMirror(wrapper));
for (VariableElement parameter : method.getParameters()) {
if (typeUtils.isAssignable(parameter.asType(), collectionOfWrapper)) {
usedInCollections.add(wrapper);
break;
}
}
}
return usedInCollections.build();
}
private TypeMirror getTypeMirror(Class<?> c) {
return elementUtils.getTypeElement(c.getName()).asType();
}
private static boolean isGwtCompatible(TypeElement annotationElement) {
return annotationElement
.getAnnotationMirrors()
.stream()
.map(mirror -> mirror.getAnnotationType().asElement())
.anyMatch(element -> element.getSimpleName().contentEquals("GwtCompatible"));
}
private static String fullyQualifiedName(String pkg, String cls) {
return pkg.isEmpty() ? cls : pkg + "." + cls;
}
private 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.
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.WARNING, "Could not write generated class " + className + ": " + e);
}
}
public static class Member {
private final ProcessingEnvironment processingEnv;
private final Element context;
private final ExecutableElement method;
Member(ProcessingEnvironment processingEnv, Element context, ExecutableElement method) {
this.processingEnv = processingEnv;
this.context = context;
this.method = method;
}
@Override
public String toString() {
return method.getSimpleName().toString();
}
public String getType() {
return TypeEncoder.encode(getTypeMirror());
}
public String getComponentType() {
Preconditions.checkState(getTypeMirror().getKind() == TypeKind.ARRAY);
ArrayType arrayType = (ArrayType) getTypeMirror();
return TypeEncoder.encode(arrayType.getComponentType());
}
public TypeMirror getTypeMirror() {
return method.getReturnType();
}
public TypeKind getKind() {
return getTypeMirror().getKind();
}
// Used as part of the hashCode() computation.
// See https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Annotation.html#hashCode--
public int getNameHash() {
return 127 * toString().hashCode();
}
public boolean isArrayOfClassWithBounds() {
if (getTypeMirror().getKind() != TypeKind.ARRAY) {
return false;
}
TypeMirror componentType = ((ArrayType) getTypeMirror()).getComponentType();
if (componentType.getKind() != TypeKind.DECLARED) {
return false;
}
DeclaredType declared = (DeclaredType) componentType;
if (!((TypeElement) processingEnv.getTypeUtils().asElement(componentType))
.getQualifiedName()
.contentEquals("java.lang.Class")) {
return false;
}
if (declared.getTypeArguments().size() != 1) {
return false;
}
TypeMirror parameter = declared.getTypeArguments().get(0);
if (parameter.getKind() != TypeKind.WILDCARD) {
return true; // for Class<Foo>
}
WildcardType wildcard = (WildcardType) parameter;
// In theory, we should check if getExtendsBound() != Object, since '?' is equivalent to
// '? extends Object', but, experimentally, neither javac or ecj will sets getExtendsBound()
// to 'Object', so there isn't a point in checking.
return wildcard.getSuperBound() != null || wildcard.getExtendsBound() != null;
}
public String getDefaultValue() {
AnnotationValue defaultValue = method.getDefaultValue();
if (defaultValue == null) {
return null;
} else {
return AnnotationOutput.sourceFormForInitializer(
defaultValue, processingEnv, method.getSimpleName().toString(), context);
}
}
}
public static class Parameter {
private final String typeName;
private final TypeKind kind;
Parameter(TypeMirror type) {
this.typeName = TypeEncoder.encode(type);
this.kind = type.getKind();
}
public String getType() {
return typeName;
}
public TypeKind getKind() {
return kind;
}
}
}