package com.airbnb.epoxy; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeName; import java.util.Arrays; import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; 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.Types; import static com.airbnb.epoxy.Utils.getMethodOnClass; import static com.airbnb.epoxy.Utils.isIterableType; import static com.airbnb.epoxy.Utils.isSubtypeOfType; import static com.airbnb.epoxy.Utils.throwError; /** Validates that an attribute implements hashCode and equals. */ class HashCodeValidator { /** * Common interfaces that can be assumed will have implementations at runtime that implement * hashCode, but that don't have it by default. */ private static final List<String> WHITE_LISTED_TYPES = Arrays.asList( "java.lang.CharSequence" ); private static final MethodSpec HASH_CODE_METHOD = MethodSpec.methodBuilder("hashCode") .returns(TypeName.INT) .build(); private static final MethodSpec EQUALS_METHOD = MethodSpec.methodBuilder("equals") .addParameter(TypeName.OBJECT, "obj") .returns(TypeName.BOOLEAN) .build(); private final Types typeUtils; HashCodeValidator(Types typeUtils) { this.typeUtils = typeUtils; } boolean implementsHashCodeAndEquals(TypeMirror mirror) { try { validateImplementsHashCode(mirror); return true; } catch (EpoxyProcessorException e) { return false; } } void validate(AttributeInfo attribute) throws EpoxyProcessorException { try { validateImplementsHashCode(attribute.getTypeMirror()); } catch (EpoxyProcessorException e) { // Append information about the attribute and class to the existing exception throwError(e.getMessage() + " (%s#%s) Epoxy requires every field annotated with " + "@EpoxyAttribute to implement equals and hashCode so that changes in the model " + "can be tracked. " + "If you want the attribute to be excluded, use " + "@EpoxyAttribute(DoNotHash). If you want to ignore this warning use " + "@EpoxyAttribute(IgnoreRequireHashCode)", attribute.getModelName(), attribute.getName()); } } private void validateImplementsHashCode(TypeMirror mirror) throws EpoxyProcessorException { if (mirror.getKind() == TypeKind.ERROR) { // The class type cannot be resolved. This may be because it is a generated epoxy model and // the class hasn't been built yet. // We just assume that the class will implement hashCode at runtime. return; } if (TypeName.get(mirror).isPrimitive()) { return; } if (mirror.getKind() == TypeKind.ARRAY) { validateArrayType((ArrayType) mirror); return; } if (!(mirror instanceof DeclaredType)) { return; } DeclaredType declaredType = (DeclaredType) mirror; Element element = typeUtils.asElement(mirror); TypeElement clazz = (TypeElement) element; if (isIterableType(clazz)) { validateIterableType(declaredType); return; } if (isAutoValueType(element)) { return; } if (isWhiteListedType(element)) { return; } if (!hasHashCodeInClassHierarchy(clazz)) { throwError("Attribute does not implement hashCode"); } if (!hasEqualsInClassHierarchy(clazz)) { throwError("Attribute does not implement equals"); } } private boolean hasHashCodeInClassHierarchy(TypeElement clazz) { ExecutableElement methodOnClass = getMethodOnClass(clazz, HASH_CODE_METHOD, typeUtils); if (methodOnClass == null) { return false; } Element implementingClass = methodOnClass.getEnclosingElement(); if (implementingClass.getSimpleName().toString().equals("Object")) { // Don't count default implementation on Object class return false; } // We don't care if the method is abstract or not, as long as it exists and it isn't the Object // implementation then the runtime value will implement it to some degree (hopefully // correctly :P) return true; } private boolean hasEqualsInClassHierarchy(TypeElement clazz) { ExecutableElement methodOnClass = getMethodOnClass(clazz, EQUALS_METHOD, typeUtils); if (methodOnClass == null) { return false; } Element implementingClass = methodOnClass.getEnclosingElement(); if (implementingClass.getSimpleName().toString().equals("Object")) { // Don't count default implementation on Object class return false; } // We don't care if the method is abstract or not, as long as it exists and it isn't the Object // implementation then the runtime value will implement it to some degree (hopefully // correctly :P) return true; } private void validateArrayType(ArrayType mirror) throws EpoxyProcessorException { // Check that the type of the array implements hashCode TypeMirror arrayType = mirror.getComponentType(); try { validateImplementsHashCode(arrayType); } catch (EpoxyProcessorException e) { throwError("Type in array does not implement hashCode. Type: %s", arrayType.toString()); } } private void validateIterableType(DeclaredType declaredType) throws EpoxyProcessorException { for (TypeMirror typeParameter : declaredType.getTypeArguments()) { // check that the type implements hashCode try { validateImplementsHashCode(typeParameter); } catch (EpoxyProcessorException e) { throwError("Type in Iterable does not implement hashCode. Type: %s", typeParameter.toString()); } } // Assume that the iterable class implements hashCode and just return } private boolean isWhiteListedType(Element element) { for (String whiteListedType : WHITE_LISTED_TYPES) { if (isSubtypeOfType(element.asType(), whiteListedType)) { return true; } } return false; } /** * Only works for classes in the module since AutoValue has a retention of Source so it is * discarded after compilation. */ private boolean isAutoValueType(Element element) { for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { DeclaredType annotationType = annotationMirror.getAnnotationType(); boolean isAutoValue = isSubtypeOfType(annotationType, "com.google.auto.value.AutoValue"); if (isAutoValue) { return true; } } return false; } }