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