/* * 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 [email protected] (É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; } } }