| package org.checkerframework.checker.calledmethods.builder; |
| |
| import com.sun.source.tree.NewClassTree; |
| import java.beans.Introspector; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| import javax.lang.model.element.AnnotationMirror; |
| 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.type.DeclaredType; |
| import javax.lang.model.type.TypeKind; |
| import javax.lang.model.type.TypeMirror; |
| import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; |
| import org.checkerframework.checker.calledmethods.qual.CalledMethods; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; |
| import org.checkerframework.framework.util.AnnotatedTypes; |
| import org.checkerframework.javacutil.AnnotationUtils; |
| import org.checkerframework.javacutil.BugInCF; |
| import org.checkerframework.javacutil.ElementUtils; |
| import org.checkerframework.javacutil.TreeUtils; |
| import org.checkerframework.javacutil.TypesUtils; |
| import org.plumelib.util.ArraysPlume; |
| |
| /** |
| * AutoValue support for the Called Methods Checker. This class adds {@code @}{@link CalledMethods} |
| * annotations to the code generated by AutoValue. |
| */ |
| public class AutoValueSupport implements BuilderFrameworkSupport { |
| |
| /** The type factory. */ |
| private CalledMethodsAnnotatedTypeFactory atypeFactory; |
| |
| /** |
| * Create a new AutoValueSupport. |
| * |
| * @param atypeFactory the typechecker's type factory |
| */ |
| public AutoValueSupport(CalledMethodsAnnotatedTypeFactory atypeFactory) { |
| this.atypeFactory = atypeFactory; |
| } |
| |
| /** |
| * This method modifies the type of a copy constructor generated by AutoValue to match the type of |
| * the AutoValue toBuilder method, and has no effect if {@code tree} is a call to any other |
| * constructor. |
| * |
| * @param tree AST for a constructor call |
| * @param type type of the call expression |
| */ |
| @Override |
| public void handleConstructor(NewClassTree tree, AnnotatedTypeMirror type) { |
| ExecutableElement element = TreeUtils.elementFromUse(tree); |
| TypeMirror superclass = ((TypeElement) element.getEnclosingElement()).getSuperclass(); |
| |
| if (superclass.getKind() != TypeKind.NONE |
| && ElementUtils.hasAnnotation( |
| TypesUtils.getTypeElement(superclass), getAutoValuePackageName() + ".AutoValue.Builder") |
| && element.getParameters().size() > 0) { |
| handleToBuilderType( |
| type, |
| superclass, |
| (TypeElement) TypesUtils.getTypeElement(superclass).getEnclosingElement()); |
| } |
| } |
| |
| @Override |
| public boolean isBuilderBuildMethod(ExecutableElement candidateBuildElement) { |
| TypeElement builderElement = (TypeElement) candidateBuildElement.getEnclosingElement(); |
| if (ElementUtils.hasAnnotation( |
| builderElement, getAutoValuePackageName() + ".AutoValue.Builder")) { |
| Element classContainingBuilderElement = builderElement.getEnclosingElement(); |
| if (!ElementUtils.hasAnnotation( |
| classContainingBuilderElement, getAutoValuePackageName() + ".AutoValue")) { |
| throw new BugInCF( |
| "class " |
| + classContainingBuilderElement.getSimpleName() |
| + " is missing @AutoValue annotation"); |
| } |
| // it is a build method if it returns the type with the @AutoValue annotation |
| if (TypesUtils.getTypeElement(candidateBuildElement.getReturnType()) |
| .equals(classContainingBuilderElement)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void handleBuilderBuildMethod(AnnotatedExecutableType builderBuildType) { |
| |
| ExecutableElement element = builderBuildType.getElement(); |
| TypeElement builderElement = (TypeElement) element.getEnclosingElement(); |
| TypeElement autoValueClassElement = (TypeElement) builderElement.getEnclosingElement(); |
| AnnotationMirror newCalledMethodsAnno = |
| createCalledMethodsForAutoValueClass(builderElement, autoValueClassElement); |
| // Only add the new @CalledMethods annotation if there is not already a @CalledMethods |
| // annotation present. |
| AnnotationMirror explicitCalledMethodsAnno = |
| builderBuildType |
| .getReceiverType() |
| .getAnnotationInHierarchy( |
| atypeFactory.getQualifierHierarchy().getTopAnnotation(newCalledMethodsAnno)); |
| if (explicitCalledMethodsAnno == null) { |
| builderBuildType.getReceiverType().addAnnotation(newCalledMethodsAnno); |
| } |
| } |
| |
| @Override |
| public boolean isToBuilderMethod(ExecutableElement candidateToBuilderElement) { |
| if (!"toBuilder".equals(candidateToBuilderElement.getSimpleName().toString())) { |
| return false; |
| } |
| |
| TypeElement candidateClassContainingToBuilder = |
| (TypeElement) candidateToBuilderElement.getEnclosingElement(); |
| boolean isAbstractAV = |
| isAutoValueGenerated(candidateClassContainingToBuilder) |
| && candidateToBuilderElement.getModifiers().contains(Modifier.ABSTRACT); |
| TypeMirror superclassOfClassContainingToBuilder = |
| candidateClassContainingToBuilder.getSuperclass(); |
| boolean superIsAV = false; |
| if (superclassOfClassContainingToBuilder.getKind() != TypeKind.NONE) { |
| superIsAV = |
| isAutoValueGenerated(TypesUtils.getTypeElement(superclassOfClassContainingToBuilder)); |
| } |
| return superIsAV || isAbstractAV; |
| } |
| |
| @Override |
| public void handleToBuilderMethod(AnnotatedExecutableType toBuilderType) { |
| AnnotatedTypeMirror returnType = toBuilderType.getReturnType(); |
| ExecutableElement toBuilderElement = toBuilderType.getElement(); |
| TypeElement classContainingToBuilder = (TypeElement) toBuilderElement.getEnclosingElement(); |
| // Because of the way that the check in #isToBuilderMethod works, if the code reaches this |
| // point and this condition is false, the other condition MUST be true (otherwise, |
| // isToBuilderMethod would have returned false). |
| if (isAutoValueGenerated(classContainingToBuilder) |
| && toBuilderElement.getModifiers().contains(Modifier.ABSTRACT)) { |
| handleToBuilderType(returnType, returnType.getUnderlyingType(), classContainingToBuilder); |
| } else { |
| TypeElement superElement = |
| TypesUtils.getTypeElement(classContainingToBuilder.getSuperclass()); |
| handleToBuilderType(returnType, returnType.getUnderlyingType(), superElement); |
| } |
| } |
| |
| /** |
| * Was the given element generated by AutoValue? |
| * |
| * @param element the element to check |
| * @return true if the element was generated by AutoValue |
| */ |
| private boolean isAutoValueGenerated(Element element) { |
| return ElementUtils.hasAnnotation(element, getAutoValuePackageName() + ".AutoValue"); |
| } |
| |
| /** |
| * Add, to {@code type}, a CalledMethods annotation with all required methods called. The type can |
| * be the return type of toBuilder or of the corresponding generated "copy" constructor. |
| * |
| * @param type type to update |
| * @param builderType type of abstract @AutoValue.Builder class |
| * @param classElement AutoValue class corresponding to {@code type} |
| */ |
| private void handleToBuilderType( |
| AnnotatedTypeMirror type, TypeMirror builderType, TypeElement classElement) { |
| TypeElement builderElement = TypesUtils.getTypeElement(builderType); |
| AnnotationMirror calledMethodsAnno = |
| createCalledMethodsForAutoValueClass(builderElement, classElement); |
| type.replaceAnnotation(calledMethodsAnno); |
| } |
| |
| /** |
| * Create an @CalledMethods annotation for the given AutoValue class and builder. The returned |
| * annotation contains all the required setters. |
| * |
| * @param builderElement the element for the Builder class |
| * @param classElement the element for the AutoValue class (i.e. the class that is built by the |
| * builder) |
| * @return an @CalledMethods annotation representing that all the required setters have been |
| * called |
| */ |
| private AnnotationMirror createCalledMethodsForAutoValueClass( |
| TypeElement builderElement, TypeElement classElement) { |
| Set<String> avBuilderSetterNames = getAutoValueBuilderSetterMethodNames(builderElement); |
| List<String> requiredProperties = |
| getAutoValueRequiredProperties(classElement, avBuilderSetterNames); |
| return createCalledMethodsForAutoValueProperties(requiredProperties, avBuilderSetterNames); |
| } |
| |
| /** |
| * Creates a @CalledMethods annotation for the given property names, converting the names to the |
| * corresponding setter method name in the Builder. |
| * |
| * @param propertyNames the property names |
| * @param avBuilderSetterNames names of all setters in the builder class |
| * @return a @CalledMethods annotation that indicates all the given properties have been set |
| */ |
| private AnnotationMirror createCalledMethodsForAutoValueProperties( |
| final List<String> propertyNames, Set<String> avBuilderSetterNames) { |
| List<String> calledMethodNames = |
| propertyNames.stream() |
| .map(prop -> autoValuePropToBuilderSetterName(prop, avBuilderSetterNames)) |
| .filter(Objects::nonNull) |
| .collect(Collectors.toList()); |
| return atypeFactory.createAccumulatorAnnotation(calledMethodNames); |
| } |
| |
| /** |
| * Converts the name of a property (i.e., a field) into the name of its setter. |
| * |
| * @param prop the property (i.e., field) name |
| * @param builderSetterNames names of all methods in the builder class |
| * @return the name of the setter for prop |
| */ |
| private static String autoValuePropToBuilderSetterName( |
| String prop, Set<String> builderSetterNames) { |
| String[] possiblePropNames; |
| if (prop.startsWith("get") && prop.length() > 3 && Character.isUpperCase(prop.charAt(3))) { |
| possiblePropNames = new String[] {prop, Introspector.decapitalize(prop.substring(3))}; |
| } else if (prop.startsWith("is") |
| && prop.length() > 2 |
| && Character.isUpperCase(prop.charAt(2))) { |
| possiblePropNames = new String[] {prop, Introspector.decapitalize(prop.substring(2))}; |
| } else { |
| possiblePropNames = new String[] {prop}; |
| } |
| |
| for (String propName : possiblePropNames) { |
| // The setter may be the property name itself, or prefixed by 'set'. |
| if (builderSetterNames.contains(propName)) { |
| return propName; |
| } |
| String setterName = "set" + BuilderFrameworkSupportUtils.capitalize(propName); |
| if (builderSetterNames.contains(setterName)) { |
| return setterName; |
| } |
| } |
| |
| // Could not find a corresponding setter. This is likely because an AutoValue Extension is in |
| // use. See https://github.com/kelloggm/object-construction-checker/issues/110 . For now we |
| // return null, but once that bug is fixed, this should be changed to an assertion failure. |
| return null; |
| } |
| |
| /** |
| * Computes the required properties of an @AutoValue class. |
| * |
| * @param autoValueClassElement the @AutoValue class |
| * @param avBuilderSetterNames names of all setters in the corresponding AutoValue builder class |
| * @return a list of required property names |
| */ |
| private List<String> getAutoValueRequiredProperties( |
| final TypeElement autoValueClassElement, Set<String> avBuilderSetterNames) { |
| return getAllAbstractMethods(autoValueClassElement).stream() |
| .filter(member -> isAutoValueRequiredProperty(member, avBuilderSetterNames)) |
| .map(e -> e.getSimpleName().toString()) |
| .collect(Collectors.toList()); |
| } |
| |
| /** |
| * Does member represent a required property of an AutoValue class? |
| * |
| * @param member a member of an AutoValue class or superclass |
| * @param avBuilderSetterNames names of all setters in corresponding AutoValue builder class |
| * @return true if {@code member} is required |
| */ |
| private boolean isAutoValueRequiredProperty(Element member, Set<String> avBuilderSetterNames) { |
| String name = member.getSimpleName().toString(); |
| // Ignore java.lang.Object overrides, constructors, and toBuilder methods in AutoValue classes. |
| // Strictly speaking, this code should check return types, etc. to handle strange |
| // overloads and other corner cases. They seem unlikely enough that we are skipping for now. |
| if (ArraysPlume.indexOf( |
| new String[] {"equals", "hashCode", "toString", "<init>", "toBuilder"}, name) |
| != -1) { |
| return false; |
| } |
| TypeMirror returnType = ((ExecutableElement) member).getReturnType(); |
| if (returnType.getKind() == TypeKind.VOID) { |
| return false; |
| } |
| // shouldn't have a nullable return |
| boolean hasNullable = |
| Stream.concat( |
| atypeFactory.getElementUtils().getAllAnnotationMirrors(member).stream(), |
| returnType.getAnnotationMirrors().stream()) |
| .anyMatch(anm -> AnnotationUtils.annotationName(anm).endsWith(".Nullable")); |
| if (hasNullable) { |
| return false; |
| } |
| // if return type of foo() is a Guava Immutable type, not required if there is a |
| // builder method fooBuilder() |
| if (BuilderFrameworkSupportUtils.isGuavaImmutableType(returnType) |
| && avBuilderSetterNames.contains(name + "Builder")) { |
| return false; |
| } |
| // if it's an Optional, the Builder will automatically initialize it |
| if (isOptional(returnType)) { |
| return false; |
| } |
| // it's required! |
| return true; |
| } |
| |
| /** |
| * Returns whether AutoValue considers a type to be "optional". Optional types do not need to be |
| * set before build is called on a builder. Adapted from AutoValue source code. |
| * |
| * @param type some type |
| * @return true if type is an Optional type |
| */ |
| static boolean isOptional(TypeMirror type) { |
| if (type.getKind() != TypeKind.DECLARED) { |
| return false; |
| } |
| DeclaredType declaredType = (DeclaredType) type; |
| TypeElement typeElement = (TypeElement) declaredType.asElement(); |
| // This list of classes that AutoValue considers "optional" comes from AutoValue's source code. |
| String[] optionalClassNames = |
| new String[] { |
| "com.google.common.base.Optional", |
| "java.util.Optional", |
| "java.util.OptionalDouble", |
| "java.util.OptionalInt", |
| "java.util.OptionalLong" |
| }; |
| return typeElement.getTypeParameters().size() == declaredType.getTypeArguments().size() |
| && ArraysPlume.indexOf(optionalClassNames, typeElement.getQualifiedName().toString()) != -1; |
| } |
| |
| /** |
| * Returns names of all setter methods. |
| * |
| * @see #isAutoValueBuilderSetter |
| * @param builderElement the element representing an AutoValue builder |
| * @return the names of setter methods for the AutoValue builder |
| */ |
| private Set<String> getAutoValueBuilderSetterMethodNames(TypeElement builderElement) { |
| return getAllAbstractMethods(builderElement).stream() |
| .filter(e -> isAutoValueBuilderSetter(e, builderElement)) |
| .map(e -> e.getSimpleName().toString()) |
| .collect(Collectors.toSet()); |
| } |
| |
| /** |
| * Return true if the given method is a setter for an AutoValue builder; that is, its return type |
| * is the builder itself or a Guava Immutable type. |
| * |
| * @param method a method of a builder or one of its supertypes |
| * @param builderElement element for the AutoValue builder |
| * @return true if {@code method} is a setter for the builder |
| */ |
| private boolean isAutoValueBuilderSetter(ExecutableElement method, TypeElement builderElement) { |
| TypeMirror retType = method.getReturnType(); |
| if (retType.getKind() == TypeKind.TYPEVAR) { |
| // instantiate the type variable for the Builder class |
| retType = |
| AnnotatedTypes.asMemberOf( |
| atypeFactory.getChecker().getTypeUtils(), |
| atypeFactory, |
| atypeFactory.getAnnotatedType(builderElement), |
| method) |
| .getReturnType() |
| .getUnderlyingType(); |
| } |
| // either the return type should be the builder itself, or it should be a Guava immutable type |
| return BuilderFrameworkSupportUtils.isGuavaImmutableType(retType) |
| || builderElement.equals(TypesUtils.getTypeElement(retType)); |
| } |
| |
| /** |
| * Get all the abstract methods for a class. This includes inherited abstract methods that are not |
| * overridden by the class or a superclass. There is no guarantee that this method will work as |
| * intended on code that implements an interface (which AutoValue classes are not supposed to do: |
| * https://github.com/google/auto/blob/master/value/userguide/howto.md#inherit). |
| * |
| * @param classElement the class |
| * @return list of all abstract methods |
| */ |
| public List<ExecutableElement> getAllAbstractMethods(TypeElement classElement) { |
| List<TypeElement> supertypes = |
| ElementUtils.getAllSupertypes(classElement, atypeFactory.getProcessingEnv()); |
| List<ExecutableElement> abstractMethods = new ArrayList<>(); |
| Set<ExecutableElement> overriddenMethods = new HashSet<>(); |
| for (Element t : supertypes) { |
| for (Element member : t.getEnclosedElements()) { |
| if (member.getKind() != ElementKind.METHOD) { |
| continue; |
| } |
| Set<Modifier> modifiers = member.getModifiers(); |
| if (modifiers.contains(Modifier.STATIC)) { |
| continue; |
| } |
| if (modifiers.contains(Modifier.ABSTRACT)) { |
| // Make sure it's not overridden. This only works because ElementUtils#closure |
| // returns results in a particular order. |
| if (!overriddenMethods.contains(member)) { |
| abstractMethods.add((ExecutableElement) member); |
| } |
| } else { |
| // Exclude any methods that this overrides. |
| overriddenMethods.addAll( |
| AnnotatedTypes.overriddenMethods( |
| atypeFactory.getElementUtils(), atypeFactory, (ExecutableElement) member) |
| .values()); |
| } |
| } |
| } |
| return abstractMethods; |
| } |
| |
| /** |
| * Get the qualified name of the package containing AutoValue annotations. This method constructs |
| * the String dynamically, to ensure it does not get rewritten due to relocation of the {@code |
| * "com.google"} package during the build process. |
| * |
| * @return {@code "com.google.auto.value"} |
| */ |
| private String getAutoValuePackageName() { |
| String com = "com"; |
| return com + "." + "google.auto.value"; |
| } |
| } |