Home | History | Annotate | Download | only in validator
      1 package org.robolectric.annotation.processing.validator;
      2 
      3 import static org.robolectric.annotation.processing.validator.ImplementationValidator.METHODS_ALLOWED_TO_BE_PUBLIC;
      4 
      5 import com.sun.source.tree.ImportTree;
      6 import com.sun.source.util.Trees;
      7 import java.util.ArrayList;
      8 import java.util.HashMap;
      9 import java.util.List;
     10 import java.util.Map;
     11 import java.util.Map.Entry;
     12 import java.util.Set;
     13 import java.util.TreeSet;
     14 import javax.annotation.processing.Messager;
     15 import javax.annotation.processing.ProcessingEnvironment;
     16 import javax.lang.model.element.AnnotationMirror;
     17 import javax.lang.model.element.AnnotationValue;
     18 import javax.lang.model.element.Element;
     19 import javax.lang.model.element.ElementKind;
     20 import javax.lang.model.element.ExecutableElement;
     21 import javax.lang.model.element.Modifier;
     22 import javax.lang.model.element.TypeElement;
     23 import javax.lang.model.element.TypeParameterElement;
     24 import javax.lang.model.element.VariableElement;
     25 import javax.lang.model.type.TypeMirror;
     26 import javax.lang.model.util.ElementFilter;
     27 import javax.lang.model.util.Elements;
     28 import javax.tools.Diagnostic.Kind;
     29 import org.robolectric.annotation.Implementation;
     30 import org.robolectric.annotation.processing.DocumentedMethod;
     31 import org.robolectric.annotation.processing.Helpers;
     32 import org.robolectric.annotation.processing.RobolectricModel;
     33 
     34 /**
     35  * Validator that checks usages of {@link org.robolectric.annotation.Implements}.
     36  */
     37 public class ImplementsValidator extends Validator {
     38 
     39   public static final String IMPLEMENTS_CLASS = "org.robolectric.annotation.Implements";
     40   public static final int MAX_SUPPORTED_ANDROID_SDK = 10000; // Now == Build.VERSION_CODES.O
     41 
     42   public static final String STATIC_INITIALIZER_METHOD_NAME = "__staticInitializer__";
     43   public static final String CONSTRUCTOR_METHOD_NAME = "__constructor__";
     44 
     45   private static final SdkStore sdkStore = new SdkStore();
     46 
     47   private final ProcessingEnvironment env;
     48   private final SdkCheckMode sdkCheckMode;
     49 
     50   /**
     51    * Supported modes for validation of {@link Implementation} methods against SDKs.
     52    */
     53   public enum SdkCheckMode {
     54     OFF,
     55     WARN,
     56     ERROR
     57   }
     58 
     59   public ImplementsValidator(RobolectricModel.Builder modelBuilder, ProcessingEnvironment env,
     60       SdkCheckMode sdkCheckMode) {
     61     super(modelBuilder, env, IMPLEMENTS_CLASS);
     62 
     63     this.env = env;
     64     this.sdkCheckMode = sdkCheckMode;
     65   }
     66 
     67   private TypeElement getClassNameTypeElement(AnnotationValue cv) {
     68     String className = Helpers.getAnnotationStringValue(cv);
     69     return elements.getTypeElement(className.replace('$', '.'));
     70   }
     71 
     72   @Override
     73   public Void visitType(TypeElement shadowType, Element parent) {
     74     captureJavadoc(shadowType);
     75 
     76     // inner class shadows must be static
     77     if (shadowType.getEnclosingElement().getKind() == ElementKind.CLASS
     78         && !shadowType.getModifiers().contains(Modifier.STATIC)) {
     79 
     80       error("inner shadow classes must be static");
     81     }
     82 
     83     // Don't import nested classes because some of them have the same name.
     84     AnnotationMirror am = getCurrentAnnotation();
     85     AnnotationValue av = Helpers.getAnnotationTypeMirrorValue(am, "value");
     86     AnnotationValue cv = Helpers.getAnnotationTypeMirrorValue(am, "className");
     87 
     88     AnnotationValue minSdkVal = Helpers.getAnnotationTypeMirrorValue(am, "minSdk");
     89     int minSdk = minSdkVal == null ? -1 : Helpers.getAnnotationIntValue(minSdkVal);
     90     AnnotationValue maxSdkVal = Helpers.getAnnotationTypeMirrorValue(am, "maxSdk");
     91     int maxSdk = maxSdkVal == null ? -1 : Helpers.getAnnotationIntValue(maxSdkVal);
     92 
     93     AnnotationValue shadowPickerValue =
     94         Helpers.getAnnotationTypeMirrorValue(am, "shadowPicker");
     95     TypeMirror shadowPickerTypeMirror = shadowPickerValue == null
     96         ? null
     97         : Helpers.getAnnotationTypeMirrorValue(shadowPickerValue);
     98 
     99     // This shadow doesn't apply to the current SDK. todo: check each SDK.
    100     if (maxSdk != -1 && maxSdk < MAX_SUPPORTED_ANDROID_SDK) {
    101       addShadowNotInSdk(shadowType, av, cv);
    102       return null;
    103     }
    104 
    105     TypeElement actualType = null;
    106     if (av == null) {
    107       if (cv == null) {
    108         error("@Implements: must specify <value> or <className>");
    109         return null;
    110       }
    111       actualType = getClassNameTypeElement(cv);
    112 
    113       if (actualType == null
    114           && !suppressWarnings(shadowType, "robolectric.internal.IgnoreMissingClass")) {
    115         error("@Implements: could not resolve class <" + cv + '>', cv);
    116         return null;
    117       }
    118     } else {
    119       TypeMirror value = Helpers.getAnnotationTypeMirrorValue(av);
    120       if (value == null) {
    121         return null;
    122       }
    123       if (cv != null) {
    124         error("@Implements: cannot specify both <value> and <className> attributes");
    125       } else {
    126         actualType = Helpers.getAnnotationTypeMirrorValue(types.asElement(value));
    127       }
    128     }
    129     if (actualType == null) {
    130       addShadowNotInSdk(shadowType, av, cv);
    131       return null;
    132     }
    133     final List<? extends TypeParameterElement> typeTP = actualType.getTypeParameters();
    134     final List<? extends TypeParameterElement> elemTP = shadowType.getTypeParameters();
    135     if (!helpers.isSameParameterList(typeTP, elemTP)) {
    136       StringBuilder message = new StringBuilder();
    137       if (elemTP.isEmpty()) {
    138         message.append("Shadow type is missing type parameters, expected <");
    139         helpers.appendParameterList(message, actualType.getTypeParameters());
    140         message.append('>');
    141       } else if (typeTP.isEmpty()) {
    142         message.append("Shadow type has type parameters but real type does not");
    143       } else {
    144         message.append("Shadow type must have same type parameters as its real counterpart: expected <");
    145         helpers.appendParameterList(message, actualType.getTypeParameters());
    146         message.append(">, was <");
    147         helpers.appendParameterList(message, shadowType.getTypeParameters());
    148         message.append('>');
    149       }
    150       messager.printMessage(Kind.ERROR, message, shadowType);
    151       return null;
    152     }
    153 
    154     AnnotationValue looseSignaturesAttr =
    155         Helpers.getAnnotationTypeMirrorValue(am, "looseSignatures");
    156     boolean looseSignatures =
    157         looseSignaturesAttr == null ? false : (Boolean) looseSignaturesAttr.getValue();
    158     validateShadowMethods(actualType, shadowType, minSdk, maxSdk, looseSignatures);
    159 
    160     modelBuilder.addShadowType(shadowType, actualType,
    161         shadowPickerTypeMirror == null
    162             ? null
    163             : (TypeElement) types.asElement(shadowPickerTypeMirror));
    164     return null;
    165   }
    166 
    167   private void addShadowNotInSdk(TypeElement shadowType, AnnotationValue av, AnnotationValue cv) {
    168     String sdkClassName;
    169     if (av == null) {
    170       sdkClassName = Helpers.getAnnotationStringValue(cv).replace('$', '.');
    171     } else {
    172       sdkClassName = av.toString();
    173     }
    174 
    175     // there's no such type at the current SDK level, so just use strings...
    176     // getQualifiedName() uses Outer.Inner and we want Outer$Inner, so:
    177     String name = getClassFQName(shadowType);
    178     modelBuilder.addExtraShadow(sdkClassName, name);
    179   }
    180 
    181   private static boolean suppressWarnings(Element element, String warningName) {
    182     SuppressWarnings[] suppressWarnings = element.getAnnotationsByType(SuppressWarnings.class);
    183     for (SuppressWarnings suppression : suppressWarnings) {
    184       for (String name : suppression.value()) {
    185         if (warningName.equals(name)) {
    186           return true;
    187         }
    188       }
    189     }
    190     return false;
    191   }
    192 
    193   static String getClassFQName(TypeElement elem) {
    194     StringBuilder name = new StringBuilder();
    195     while (isClassy(elem.getEnclosingElement().getKind())) {
    196       name.insert(0, "$" + elem.getSimpleName());
    197       elem = (TypeElement) elem.getEnclosingElement();
    198     }
    199     name.insert(0, elem.getQualifiedName());
    200     return name.toString();
    201   }
    202 
    203   private static boolean isClassy(ElementKind kind) {
    204     return kind == ElementKind.CLASS || kind == ElementKind.INTERFACE;
    205   }
    206 
    207   private void validateShadowMethods(TypeElement sdkClassElem, TypeElement shadowClassElem,
    208       int classMinSdk, int classMaxSdk, boolean looseSignatures) {
    209     for (Element memberElement : ElementFilter.methodsIn(shadowClassElem.getEnclosedElements())) {
    210       ExecutableElement methodElement = (ExecutableElement) memberElement;
    211 
    212       // equals, hashCode, and toString are exempt, because of Robolectric's weird special behavior
    213       if (METHODS_ALLOWED_TO_BE_PUBLIC.contains(methodElement.getSimpleName().toString())) {
    214         continue;
    215       }
    216 
    217       verifySdkMethod(sdkClassElem, methodElement, classMinSdk, classMaxSdk, looseSignatures);
    218 
    219       String methodName = methodElement.getSimpleName().toString();
    220       if (methodName.equals(CONSTRUCTOR_METHOD_NAME)
    221           || methodName.equals(STATIC_INITIALIZER_METHOD_NAME)) {
    222         Implementation implementation = memberElement.getAnnotation(Implementation.class);
    223         if (implementation == null) {
    224           messager.printMessage(
    225               Kind.ERROR, "Shadow methods must be annotated @Implementation", methodElement);
    226         }
    227       }
    228     }
    229   }
    230 
    231   private void verifySdkMethod(TypeElement sdkClassElem, ExecutableElement methodElement,
    232       int classMinSdk, int classMaxSdk, boolean looseSignatures) {
    233     if (sdkCheckMode == SdkCheckMode.OFF) {
    234       return;
    235     }
    236 
    237     Implementation implementation = methodElement.getAnnotation(Implementation.class);
    238     if (implementation != null) {
    239       Kind kind = sdkCheckMode == SdkCheckMode.WARN
    240           ? Kind.WARNING
    241           : Kind.ERROR;
    242       Problems problems = new Problems(kind);
    243 
    244       for (SdkStore.Sdk sdk : sdkStore.sdksMatching(implementation, classMinSdk, classMaxSdk)) {
    245         String problem = sdk.verifyMethod(sdkClassElem, methodElement, looseSignatures);
    246         if (problem != null) {
    247           problems.add(problem, sdk.sdkInt);
    248         }
    249       }
    250 
    251       if (problems.any()) {
    252         problems.recount(messager, methodElement);
    253       }
    254     }
    255   }
    256 
    257   private void captureJavadoc(TypeElement elem) {
    258     List<String> imports = new ArrayList<>();
    259     List<? extends ImportTree> importLines = Trees.instance(env).getPath(elem).getCompilationUnit().getImports();
    260     for (ImportTree importLine : importLines) {
    261       imports.add(importLine.getQualifiedIdentifier().toString());
    262     }
    263 
    264     List<TypeElement> enclosedTypes = ElementFilter.typesIn(elem.getEnclosedElements());
    265     for (TypeElement enclosedType : enclosedTypes) {
    266       imports.add(enclosedType.getQualifiedName().toString());
    267     }
    268 
    269     Elements elementUtils = env.getElementUtils();
    270     modelBuilder.documentType(elem, elementUtils.getDocComment(elem), imports);
    271 
    272     for (Element memberElement : ElementFilter.methodsIn(elem.getEnclosedElements())) {
    273       try {
    274         ExecutableElement methodElement = (ExecutableElement) memberElement;
    275         Implementation implementation = memberElement.getAnnotation(Implementation.class);
    276 
    277         DocumentedMethod documentedMethod = new DocumentedMethod(memberElement.toString());
    278         for (Modifier modifier : memberElement.getModifiers()) {
    279           documentedMethod.modifiers.add(modifier.toString());
    280         }
    281         documentedMethod.isImplementation = implementation != null;
    282         if (implementation != null) {
    283           documentedMethod.minSdk = sdkOrNull(implementation.minSdk());
    284           documentedMethod.maxSdk = sdkOrNull(implementation.maxSdk());
    285         }
    286         for (VariableElement variableElement : methodElement.getParameters()) {
    287           documentedMethod.params.add(variableElement.toString());
    288         }
    289         documentedMethod.returnType = methodElement.getReturnType().toString();
    290         for (TypeMirror typeMirror : methodElement.getThrownTypes()) {
    291           documentedMethod.exceptions.add(typeMirror.toString());
    292         }
    293         String docMd = elementUtils.getDocComment(methodElement);
    294         if (docMd != null) {
    295           documentedMethod.setDocumentation(docMd);
    296         }
    297 
    298         modelBuilder.documentMethod(elem, documentedMethod);
    299       } catch (Exception e) {
    300         throw new RuntimeException(
    301             "failed to capture javadoc for " + elem + "." + memberElement, e);
    302       }
    303     }
    304   }
    305 
    306   private Integer sdkOrNull(int sdk) {
    307     return sdk == -1 ? null : sdk;
    308   }
    309 
    310   private static class Problems {
    311     private final Kind kind;
    312     private final Map<String, Set<Integer>> problems = new HashMap<>();
    313 
    314     public Problems(Kind kind) {
    315       this.kind = kind;
    316     }
    317 
    318     void add(String problem, int sdkInt) {
    319       Set<Integer> sdks = problems.get(problem);
    320       if (sdks == null) {
    321         problems.put(problem, sdks = new TreeSet<>());
    322       }
    323       sdks.add(sdkInt);
    324     }
    325 
    326     boolean any() {
    327       return !problems.isEmpty();
    328     }
    329 
    330     void recount(Messager messager, Element element) {
    331       for (Entry<String, Set<Integer>> e : problems.entrySet()) {
    332         String problem = e.getKey();
    333         Set<Integer> sdks = e.getValue();
    334 
    335         StringBuilder buf = new StringBuilder();
    336         buf.append(problem)
    337             .append(" for ")
    338             .append(sdks.size() == 1 ? "SDK " : "SDKs ");
    339 
    340         Integer previousSdk = null;
    341         Integer lastSdk = null;
    342         for (Integer sdk : sdks) {
    343           if (previousSdk == null) {
    344             buf.append(sdk);
    345           } else {
    346             if (previousSdk != sdk - 1) {
    347               buf.append("-").append(previousSdk);
    348               buf.append("/").append(sdk);
    349               lastSdk = null;
    350             } else {
    351               lastSdk = sdk;
    352             }
    353           }
    354 
    355           previousSdk = sdk;
    356         }
    357 
    358         if (lastSdk != null) {
    359           buf.append("-").append(lastSdk);
    360         }
    361 
    362         messager.printMessage(kind, buf.toString(), element);
    363       }
    364     }
    365   }
    366 }
    367