| /* |
| * 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; |
| } |
| } |
| } |