Home | History | Annotate | Download | only in processing
      1 package org.robolectric.annotation.processing;
      2 
      3 import static com.google.common.collect.Maps.newHashMap;
      4 import static com.google.common.collect.Maps.newTreeMap;
      5 import static com.google.common.collect.Sets.newTreeSet;
      6 
      7 import com.google.common.collect.HashMultimap;
      8 import com.google.common.collect.Iterables;
      9 import com.google.common.collect.Multimaps;
     10 import java.util.Collection;
     11 import java.util.List;
     12 import java.util.Map;
     13 import java.util.Set;
     14 import java.util.TreeMap;
     15 import java.util.TreeSet;
     16 import javax.annotation.processing.ProcessingEnvironment;
     17 import javax.lang.model.element.Element;
     18 import javax.lang.model.element.ElementVisitor;
     19 import javax.lang.model.element.ExecutableElement;
     20 import javax.lang.model.element.Modifier;
     21 import javax.lang.model.element.PackageElement;
     22 import javax.lang.model.element.TypeElement;
     23 import javax.lang.model.element.TypeParameterElement;
     24 import javax.lang.model.type.DeclaredType;
     25 import javax.lang.model.type.TypeMirror;
     26 import javax.lang.model.type.TypeVisitor;
     27 import javax.lang.model.util.SimpleElementVisitor6;
     28 import javax.lang.model.util.SimpleTypeVisitor6;
     29 import org.robolectric.annotation.Implements;
     30 import org.robolectric.shadow.api.ShadowPicker;
     31 
     32 /**
     33  * Model describing the Robolectric source file.
     34  */
     35 public class RobolectricModel {
     36 
     37   private final TreeSet<String> imports;
     38   /**
     39    * Key: name of shadow class
     40    */
     41   private final TreeMap<String, ShadowInfo> shadowTypes;
     42   private final TreeMap<String, String> extraShadowTypes;
     43   /**
     44    * Key: name of shadow class
     45    */
     46   private final TreeMap<String, ResetterInfo> resetterMap;
     47 
     48   private final TreeMap<String, DocumentedPackage> documentedPackages;
     49 
     50   public Collection<DocumentedPackage> getDocumentedPackages() {
     51     return documentedPackages.values();
     52   }
     53 
     54   public RobolectricModel(TreeSet<String> imports,
     55       TreeMap<String, ShadowInfo> shadowTypes,
     56       TreeMap<String, String> extraShadowTypes,
     57       TreeMap<String, ResetterInfo> resetterMap,
     58       Map<String, DocumentedPackage> documentedPackages) {
     59     this.imports = new TreeSet<>(imports);
     60     this.shadowTypes = new TreeMap<>(shadowTypes);
     61     this.extraShadowTypes = new TreeMap<>(extraShadowTypes);
     62     this.resetterMap = new TreeMap<>(resetterMap);
     63     this.documentedPackages = new TreeMap<>(documentedPackages);
     64   }
     65 
     66   private final static ElementVisitor<TypeElement, Void> TYPE_ELEMENT_VISITOR =
     67       new SimpleElementVisitor6<TypeElement, Void>() {
     68         @Override
     69         public TypeElement visitType(TypeElement e, Void p) {
     70           return e;
     71         }
     72       };
     73 
     74   public static class Builder {
     75 
     76     private final Helpers helpers;
     77 
     78     private final TreeSet<String> imports = newTreeSet();
     79     private final TreeMap<String, ShadowInfo> shadowTypes = newTreeMap();
     80     private final TreeMap<String, String> extraShadowTypes = newTreeMap();
     81     private final TreeMap<String, ResetterInfo> resetterMap = newTreeMap();
     82     private final Map<String, DocumentedPackage> documentedPackages = new TreeMap<>();
     83 
     84     private final Map<TypeElement, TypeElement> importMap = newHashMap();
     85     private final Map<TypeElement, String> referentMap = newHashMap();
     86     private HashMultimap<String, TypeElement> typeMap = HashMultimap.create();
     87 
     88     Builder(ProcessingEnvironment environment) {
     89       this.helpers = new Helpers(environment);
     90     }
     91 
     92     public void addShadowType(TypeElement shadowType, TypeElement actualType,
     93         TypeElement shadowPickerType) {
     94       TypeElement shadowBaseType = null;
     95       if (shadowPickerType != null) {
     96         TypeMirror iface = helpers.findInterface(shadowPickerType, ShadowPicker.class);
     97         if (iface != null) {
     98           com.sun.tools.javac.code.Type type = ((com.sun.tools.javac.code.Type.ClassType) iface)
     99               .allparams().get(0);
    100           String baseClassName = type.asElement().getQualifiedName().toString();
    101           shadowBaseType = helpers.getTypeElement(baseClassName);
    102         }
    103       }
    104       ShadowInfo shadowInfo =
    105           new ShadowInfo(shadowType, actualType, shadowPickerType, shadowBaseType);
    106 
    107       if (shadowInfo.isInAndroidSdk()) {
    108         registerType(shadowInfo.shadowType);
    109         registerType(shadowInfo.actualType);
    110         registerType(shadowInfo.shadowBaseClass);
    111       }
    112 
    113       shadowTypes.put(shadowType.getQualifiedName().toString(), shadowInfo);
    114     }
    115 
    116     public void addExtraShadow(String sdkClassName, String shadowClassName) {
    117       extraShadowTypes.put(shadowClassName, sdkClassName);
    118     }
    119 
    120     public void addResetter(TypeElement shadowTypeElement, ExecutableElement elem) {
    121       registerType(shadowTypeElement);
    122 
    123       resetterMap.put(shadowTypeElement.getQualifiedName().toString(),
    124           new ResetterInfo(shadowTypeElement, elem));
    125     }
    126 
    127     public void documentPackage(String name, String documentation) {
    128       getDocumentedPackage(name).setDocumentation(documentation);
    129     }
    130 
    131     public void documentType(TypeElement type, String documentation, List<String> imports) {
    132       DocumentedType documentedType = getDocumentedType(type);
    133       documentedType.setDocumentation(documentation);
    134       documentedType.imports = imports;
    135     }
    136 
    137     public void documentMethod(TypeElement shadowClass, DocumentedMethod documentedMethod) {
    138       DocumentedType documentedType = getDocumentedType(shadowClass);
    139       documentedType.methods.put(documentedMethod.getName(), documentedMethod);
    140     }
    141 
    142     private DocumentedPackage getDocumentedPackage(String name) {
    143       DocumentedPackage documentedPackage = documentedPackages.get(name);
    144       if (documentedPackage == null) {
    145         documentedPackage = new DocumentedPackage(name);
    146         documentedPackages.put(name, documentedPackage);
    147       }
    148       return documentedPackage;
    149     }
    150 
    151     private DocumentedPackage getDocumentedPackage(TypeElement type) {
    152       Element pkgElement = type.getEnclosingElement();
    153       return getDocumentedPackage(pkgElement.toString());
    154     }
    155 
    156     private DocumentedType getDocumentedType(TypeElement type) {
    157       DocumentedPackage documentedPackage = getDocumentedPackage(type);
    158       return documentedPackage.getDocumentedType(type.getQualifiedName().toString());
    159     }
    160 
    161     RobolectricModel build() {
    162       prepare();
    163 
    164       return new RobolectricModel(imports, shadowTypes, extraShadowTypes, resetterMap,
    165           documentedPackages);
    166     }
    167 
    168     /**
    169      * Prepares the various derived parts of the model based on the class mappings that have been
    170      * registered to date.
    171      */
    172     void prepare() {
    173       while (!typeMap.isEmpty()) {
    174         final HashMultimap<String, TypeElement> nextRound = HashMultimap.create();
    175         for (Map.Entry<String, Set<TypeElement>> referents : Multimaps.asMap(typeMap).entrySet()) {
    176           final Set<TypeElement> c = referents.getValue();
    177           // If there is only one type left with the given simple
    178           // name, then
    179           if (c.size() == 1) {
    180             final TypeElement type = c.iterator().next();
    181             referentMap.put(type, referents.getKey());
    182           } else {
    183             for (TypeElement type : c) {
    184               SimpleElementVisitor6<Void, TypeElement> visitor = new SimpleElementVisitor6<Void, TypeElement>() {
    185                 @Override
    186                 public Void visitType(TypeElement parent, TypeElement type) {
    187                   nextRound.put(parent.getSimpleName() + "." + type.getSimpleName(), type);
    188                   importMap.put(type, parent);
    189                   return null;
    190                 }
    191 
    192                 @Override
    193                 public Void visitPackage(PackageElement parent, TypeElement type) {
    194                   referentMap.put(type, type.getQualifiedName().toString());
    195                   importMap.remove(type);
    196                   return null;
    197                 }
    198               };
    199               visitor.visit(importMap.get(type).getEnclosingElement(), type);
    200             }
    201           }
    202         }
    203         typeMap = nextRound;
    204       }
    205 
    206       // FIXME: check this type lookup for NPEs (and also the ones in the validators)
    207       Element javaLang = helpers.getPackageElement("java.lang");
    208 
    209       for (TypeElement imp : importMap.values()) {
    210         if (imp.getModifiers().contains(Modifier.PUBLIC)
    211             && !javaLang.equals(imp.getEnclosingElement())) {
    212           imports.add(imp.getQualifiedName().toString());
    213         }
    214       }
    215 
    216       // Other imports that the generated class needs
    217       imports.add("java.util.Map");
    218       imports.add("java.util.HashMap");
    219       imports.add("javax.annotation.Generated");
    220       imports.add("org.robolectric.internal.ShadowProvider");
    221       imports.add("org.robolectric.shadow.api.Shadow");
    222 
    223       ReferentResolver referentResolver = new ReferentResolver() {
    224         @Override
    225         public String getReferentFor(TypeMirror typeMirror) {
    226           return findReferent.visit(typeMirror);
    227         }
    228 
    229         @Override
    230         public String getReferentFor(TypeElement type) {
    231           return referentMap.get(type);
    232         }
    233       };
    234       shadowTypes.values().forEach(shadowInfo -> shadowInfo.prepare(referentResolver, helpers));
    235       resetterMap.values().forEach(resetterInfo -> resetterInfo.prepare(referentResolver));
    236     }
    237 
    238     private void registerType(TypeElement type) {
    239       if (type != null && !importMap.containsKey(type)) {
    240         typeMap.put(type.getSimpleName().toString(), type);
    241         importMap.put(type, type);
    242         for (TypeParameterElement typeParam : type.getTypeParameters()) {
    243           for (TypeMirror bound : typeParam.getBounds()) {
    244             // FIXME: get rid of cast using a visitor
    245             TypeElement boundElement = TYPE_ELEMENT_VISITOR.visit(helpers.asElement(bound));
    246             registerType(boundElement);
    247           }
    248         }
    249       }
    250     }
    251 
    252     private final TypeVisitor<String, Void> findReferent = new SimpleTypeVisitor6<String, Void>() {
    253       @Override
    254       public String visitDeclared(DeclaredType t, Void p) {
    255         return referentMap.get(t.asElement());
    256       }
    257     };
    258   }
    259 
    260   public Collection<ResetterInfo> getResetters() {
    261     return resetterMap.values();
    262   }
    263 
    264   public Set<String> getImports() {
    265     return imports;
    266   }
    267 
    268   public Collection<ShadowInfo> getAllShadowTypes() {
    269     return shadowTypes.values();
    270   }
    271 
    272   public Map<String, String> getExtraShadowTypes() {
    273     return extraShadowTypes;
    274   }
    275 
    276   public Iterable<ShadowInfo> getVisibleShadowTypes() {
    277     return Iterables.filter(shadowTypes.values(),
    278         ShadowInfo::isInAndroidSdk);
    279   }
    280 
    281   public TreeMap<String, ShadowInfo> getShadowPickers() {
    282     TreeMap<String, ShadowInfo> map = new TreeMap<>();
    283     Iterables.filter(shadowTypes.values(), ShadowInfo::hasShadowPicker)
    284         .forEach(shadowInfo -> {
    285           String actualName = shadowInfo.getActualName();
    286           String shadowPickerClassName = shadowInfo.getShadowPickerBinaryName();
    287           ShadowInfo otherShadowInfo = map.get(actualName);
    288           String otherPicker =
    289               otherShadowInfo == null ? null : otherShadowInfo.getShadowPickerBinaryName();
    290           if (otherPicker != null && !otherPicker.equals(shadowPickerClassName)) {
    291             throw new IllegalArgumentException(
    292                 actualName + " has conflicting pickers: " + shadowPickerClassName + " != "
    293                     + otherPicker);
    294           } else {
    295             map.put(actualName, shadowInfo);
    296           }
    297         });
    298     return map;
    299   }
    300 
    301   public Collection<String> getShadowedPackages() {
    302     Set<String> packages = new TreeSet<>();
    303     for (ShadowInfo shadowInfo : shadowTypes.values()) {
    304       String packageName = shadowInfo.getActualPackage();
    305 
    306       // org.robolectric.* should never be instrumented
    307       if (packageName.matches("org.robolectric(\\..*)?")) {
    308         continue;
    309       }
    310 
    311       packages.add("\"" + packageName + "\"");
    312     }
    313     return packages;
    314   }
    315 
    316   interface ReferentResolver {
    317 
    318     String getReferentFor(TypeMirror typeMirror);
    319 
    320     /**
    321      * Returns a plain string to be used in the generated source to identify the given type. The
    322      * returned string will have sufficient level of qualification in order to make the referent
    323      * unique for the source file.
    324      */
    325     String getReferentFor(TypeElement type);
    326   }
    327 
    328   public static class ShadowInfo {
    329 
    330     private final TypeElement shadowType;
    331     private final TypeElement actualType;
    332     private final TypeElement shadowPickerType;
    333     private final TypeElement shadowBaseClass;
    334 
    335     private String paramDefStr;
    336     private String paramUseStr;
    337     private String actualBinaryName;
    338     private String actualTypeReferent;
    339     private String shadowTypeReferent;
    340     private String actualTypePackage;
    341     private String shadowBinaryName;
    342     private String shadowPickerBinaryName;
    343     private String shadowBaseName;
    344 
    345     ShadowInfo(TypeElement shadowType, TypeElement actualType, TypeElement shadowPickerType,
    346         TypeElement shadowBaseClass) {
    347       this.shadowType = shadowType;
    348       this.actualType = actualType;
    349       this.shadowPickerType = shadowPickerType;
    350       this.shadowBaseClass = shadowBaseClass;
    351     }
    352 
    353     void prepare(ReferentResolver referentResolver, Helpers helpers) {
    354       int paramCount = 0;
    355       StringBuilder paramDef = new StringBuilder("<");
    356       StringBuilder paramUse = new StringBuilder("<");
    357       for (TypeParameterElement typeParam : actualType.getTypeParameters()) {
    358         if (paramCount > 0) {
    359           paramDef.append(',');
    360           paramUse.append(',');
    361         }
    362         boolean first = true;
    363         paramDef.append(typeParam);
    364         paramUse.append(typeParam);
    365         for (TypeMirror bound : helpers.getExplicitBounds(typeParam)) {
    366           if (first) {
    367             paramDef.append(" extends ");
    368             first = false;
    369           } else {
    370             paramDef.append(" & ");
    371           }
    372           paramDef.append(referentResolver.getReferentFor(bound));
    373         }
    374         paramCount++;
    375       }
    376 
    377       this.paramDefStr = "";
    378       this.paramUseStr = "";
    379       if (paramCount > 0) {
    380         paramDefStr = paramDef.append('>').toString();
    381         paramUseStr = paramUse.append('>').toString();
    382       }
    383 
    384       actualTypeReferent = referentResolver.getReferentFor(actualType);
    385       actualTypePackage = helpers.getPackageOf(actualType);
    386       actualBinaryName = helpers.getBinaryName(actualType);
    387       shadowTypeReferent = referentResolver.getReferentFor(shadowType);
    388       shadowBinaryName = helpers.getBinaryName(shadowType);
    389       shadowPickerBinaryName = helpers.getBinaryName(shadowPickerType);
    390       shadowBaseName = referentResolver.getReferentFor(shadowBaseClass);
    391     }
    392 
    393     public String getActualBinaryName() {
    394       return actualBinaryName;
    395     }
    396 
    397     public String getActualName() {
    398       return actualType.getQualifiedName().toString();
    399     }
    400 
    401     public boolean isInAndroidSdk() {
    402       return shadowType.getAnnotation(Implements.class).isInAndroidSdk();
    403     }
    404 
    405     public String getParamDefStr() {
    406       return paramDefStr;
    407     }
    408 
    409     public boolean shadowIsDeprecated() {
    410       return shadowType.getAnnotation(Deprecated.class) != null;
    411     }
    412 
    413     public boolean actualIsPublic() {
    414       return actualType.getModifiers().contains(Modifier.PUBLIC);
    415     }
    416 
    417     public String getActualTypeWithParams() {
    418       return actualTypeReferent + paramUseStr;
    419     }
    420 
    421     public String getShadowName() {
    422       return shadowType.getQualifiedName().toString();
    423     }
    424 
    425     public String getShadowBinaryName() {
    426       return shadowBinaryName;
    427     }
    428 
    429     public String getShadowTypeWithParams() {
    430       return shadowTypeReferent + paramUseStr;
    431     }
    432 
    433     String getActualPackage() {
    434       return actualTypePackage;
    435     }
    436 
    437     boolean hasShadowPicker() {
    438       return shadowPickerType != null;
    439     }
    440 
    441     public String getShadowPickerBinaryName() {
    442       return shadowPickerBinaryName;
    443     }
    444 
    445     public String getShadowBaseName() {
    446       return shadowBaseName;
    447     }
    448   }
    449 
    450   public static class ResetterInfo {
    451 
    452     private final TypeElement shadowType;
    453     private final ExecutableElement executableElement;
    454     private String shadowTypeReferent;
    455 
    456     ResetterInfo(TypeElement shadowType, ExecutableElement executableElement) {
    457       this.shadowType = shadowType;
    458       this.executableElement = executableElement;
    459     }
    460 
    461     void prepare(ReferentResolver referentResolver) {
    462       shadowTypeReferent = referentResolver.getReferentFor(shadowType);
    463     }
    464 
    465     private Implements getImplementsAnnotation() {
    466       return shadowType.getAnnotation(Implements.class);
    467     }
    468 
    469     public String getMethodCall() {
    470       return shadowTypeReferent + "." + executableElement.getSimpleName() + "();";
    471     }
    472 
    473     public int getMinSdk() {
    474       return getImplementsAnnotation().minSdk();
    475     }
    476 
    477     public int getMaxSdk() {
    478       return getImplementsAnnotation().maxSdk();
    479     }
    480   }
    481 
    482 }
    483