• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.google.auto.common.MoreElements;
6 import com.sun.source.tree.ImportTree;
7 import com.sun.source.util.Trees;
8 import java.util.ArrayList;
9 import java.util.HashMap;
10 import java.util.List;
11 import java.util.Map;
12 import java.util.Map.Entry;
13 import java.util.Set;
14 import java.util.TreeSet;
15 import javax.annotation.processing.Messager;
16 import javax.annotation.processing.ProcessingEnvironment;
17 import javax.lang.model.element.AnnotationMirror;
18 import javax.lang.model.element.AnnotationValue;
19 import javax.lang.model.element.Element;
20 import javax.lang.model.element.ElementKind;
21 import javax.lang.model.element.ExecutableElement;
22 import javax.lang.model.element.Modifier;
23 import javax.lang.model.element.TypeElement;
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 /** Validator that checks usages of {@link org.robolectric.annotation.Implements}. */
35 public class ImplementsValidator extends Validator {
36 
37   public static final String IMPLEMENTS_CLASS = "org.robolectric.annotation.Implements";
38   public static final int MAX_SUPPORTED_ANDROID_SDK = 10000; // Now == Build.VERSION_CODES.O
39 
40   public static final String STATIC_INITIALIZER_METHOD_NAME = "__staticInitializer__";
41   public static final String CONSTRUCTOR_METHOD_NAME = "__constructor__";
42 
43   private final ProcessingEnvironment env;
44   private final SdkCheckMode sdkCheckMode;
45   private final Kind checkKind;
46   private final SdkStore sdkStore;
47   private final boolean allowInDev;
48   private final boolean allowLooseSignatures;
49 
50   /** Supported modes for validation of {@link Implementation} methods against SDKs. */
51   public enum SdkCheckMode {
52     OFF,
53     WARN,
54     ERROR
55   }
56 
ImplementsValidator( RobolectricModel.Builder modelBuilder, ProcessingEnvironment env, SdkCheckMode sdkCheckMode, SdkStore sdkStore, boolean allowInDev, boolean allowLooseSignatures)57   public ImplementsValidator(
58       RobolectricModel.Builder modelBuilder,
59       ProcessingEnvironment env,
60       SdkCheckMode sdkCheckMode,
61       SdkStore sdkStore,
62       boolean allowInDev,
63       boolean allowLooseSignatures) {
64     super(modelBuilder, env, IMPLEMENTS_CLASS);
65 
66     this.env = env;
67     this.sdkCheckMode = sdkCheckMode;
68     this.checkKind = sdkCheckMode == SdkCheckMode.WARN ? Kind.WARNING : Kind.ERROR;
69     this.sdkStore = sdkStore;
70     this.allowInDev = allowInDev;
71     this.allowLooseSignatures = allowLooseSignatures;
72   }
73 
getClassNameTypeElement(AnnotationValue cv)74   private TypeElement getClassNameTypeElement(AnnotationValue cv) {
75     String className = Helpers.getAnnotationStringValue(cv);
76     return elements.getTypeElement(className.replace('$', '.'));
77   }
78 
sdkClassNameFq(AnnotationValue valueAttr, AnnotationValue classNameAttr)79   public String sdkClassNameFq(AnnotationValue valueAttr, AnnotationValue classNameAttr) {
80     String sdkClassNameFq;
81     if (valueAttr == null) {
82       sdkClassNameFq = Helpers.getAnnotationStringValue(classNameAttr);
83     } else {
84       TypeMirror typeMirror = Helpers.getAnnotationTypeMirrorValue(valueAttr);
85       TypeElement typeElement = MoreElements.asType(types.asElement(typeMirror));
86       sdkClassNameFq = elements.getBinaryName(typeElement).toString();
87     }
88     return sdkClassNameFq;
89   }
90 
91   @Override
visitType(TypeElement shadowType, Element parent)92   public Void visitType(TypeElement shadowType, Element parent) {
93     captureJavadoc(shadowType);
94 
95     // inner class shadows must be static
96     if (shadowType.getEnclosingElement().getKind() == ElementKind.CLASS
97         && !shadowType.getModifiers().contains(Modifier.STATIC)) {
98 
99       error("inner shadow classes must be static");
100     }
101 
102     // Don't import nested classes because some of them have the same name.
103     AnnotationMirror am = getCurrentAnnotation();
104     AnnotationValue av = Helpers.getAnnotationTypeMirrorValue(am, "value");
105     AnnotationValue cv = Helpers.getAnnotationTypeMirrorValue(am, "className");
106 
107     AnnotationValue minSdkVal = Helpers.getAnnotationTypeMirrorValue(am, "minSdk");
108     int minSdk = minSdkVal == null ? -1 : Helpers.getAnnotationIntValue(minSdkVal);
109     AnnotationValue maxSdkVal = Helpers.getAnnotationTypeMirrorValue(am, "maxSdk");
110     int maxSdk = maxSdkVal == null ? -1 : Helpers.getAnnotationIntValue(maxSdkVal);
111 
112     AnnotationValue shadowPickerValue = Helpers.getAnnotationTypeMirrorValue(am, "shadowPicker");
113 
114     TypeElement shadowPickerTypeElement =
115         shadowPickerValue == null
116             ? null
117             : (TypeElement)
118                 types.asElement(Helpers.getAnnotationTypeMirrorValue(shadowPickerValue));
119 
120     TypeElement actualType = null;
121     if (av == null) {
122       if (cv == null) {
123         error("@Implements: must specify <value> or <className>");
124         return null;
125       }
126       actualType = getClassNameTypeElement(cv);
127     } else {
128       TypeMirror value = Helpers.getAnnotationTypeMirrorValue(av);
129       if (value == null) {
130         return null;
131       }
132       if (cv != null) {
133         error("@Implements: cannot specify both <value> and <className> attributes");
134       } else {
135         actualType = Helpers.getAnnotationTypeMirrorValue(types.asElement(value));
136       }
137     }
138     // Checking for a public modifier here is a bit of a hack to prevent extraneous imports
139     // from appearing in the generated files.
140     // The version check is even more of a hack, and should be revisited as the
141     // output in Robolectric_ShadowPickers.java makes little to no sense.
142     if (actualType == null
143         || !actualType.getModifiers().contains(Modifier.PUBLIC)
144         || (maxSdk != -1 && maxSdk < MAX_SUPPORTED_ANDROID_SDK)) {
145       addShadowNotInSdk(shadowType, av, cv, shadowPickerTypeElement);
146     } else {
147       modelBuilder.addShadowType(shadowType, actualType, shadowPickerTypeElement);
148     }
149 
150     AnnotationValue looseSignaturesAttr =
151         Helpers.getAnnotationTypeMirrorValue(am, "looseSignatures");
152     boolean looseSignatures =
153         looseSignaturesAttr != null && (Boolean) looseSignaturesAttr.getValue();
154     if (looseSignatures && !allowLooseSignatures) {
155       error(
156           "looseSignatures is no longer allowed. Please use @ClassName or"
157               + " @Implementation(methodName = ...) instead.");
158     }
159     String sdkClassNameFq = sdkClassNameFq(av, cv);
160     validateShadow(sdkClassNameFq, shadowType, minSdk, maxSdk, looseSignatures, allowInDev);
161 
162     return null;
163   }
164 
addShadowNotInSdk( TypeElement shadowType, AnnotationValue valueAttr, AnnotationValue classNameAttr, TypeElement shadowPickerTypeElement)165   private void addShadowNotInSdk(
166       TypeElement shadowType,
167       AnnotationValue valueAttr,
168       AnnotationValue classNameAttr,
169       TypeElement shadowPickerTypeElement) {
170 
171     String sdkClassNameFq;
172     if (valueAttr == null) {
173       sdkClassNameFq = Helpers.getAnnotationStringValue(classNameAttr);
174     } else {
175       TypeMirror typeMirror = Helpers.getAnnotationTypeMirrorValue(valueAttr);
176       TypeElement typeElement = MoreElements.asType(types.asElement(typeMirror));
177       sdkClassNameFq = elements.getBinaryName(typeElement).toString();
178     }
179 
180     // there's no such type at the current SDK level, so just use strings...
181     // getQualifiedName() uses Outer.Inner and we want Outer$Inner, so:
182     String name = getClassFQName(shadowType);
183     // SHADOW_MAP currently uses class dot syntax for keys, but SHADOW_PICKER_MAP uses
184     // FQ syntax for keys.
185     modelBuilder.addExtraShadow(sdkClassNameFq.replace('$', '.'), name);
186     if (shadowPickerTypeElement != null) {
187       modelBuilder.addExtraShadowPicker(sdkClassNameFq, shadowPickerTypeElement);
188     }
189   }
190 
getClassFQName(TypeElement elem)191   static String getClassFQName(TypeElement elem) {
192     StringBuilder name = new StringBuilder();
193     while (isClassy(elem.getEnclosingElement().getKind())) {
194       name.insert(0, "$" + elem.getSimpleName());
195       elem = (TypeElement) elem.getEnclosingElement();
196     }
197     name.insert(0, elem.getQualifiedName());
198     return name.toString();
199   }
200 
isClassy(ElementKind kind)201   private static boolean isClassy(ElementKind kind) {
202     return kind == ElementKind.CLASS || kind == ElementKind.INTERFACE;
203   }
204 
validateShadow( String shadowedClassName, TypeElement shadowClassElem, int classMinSdk, int classMaxSdk, boolean looseSignatures, boolean allowInDev)205   private void validateShadow(
206       String shadowedClassName,
207       TypeElement shadowClassElem,
208       int classMinSdk,
209       int classMaxSdk,
210       boolean looseSignatures,
211       boolean allowInDev) {
212     Problems problems = new Problems(this.checkKind);
213     if (sdkCheckMode != SdkCheckMode.OFF) {
214       for (SdkStore.Sdk sdk : sdkStore.sdksMatching(classMinSdk, classMaxSdk)) {
215         SdkStore.ClassInfo classInfo = sdk.getClassInfo(shadowedClassName);
216         if (classInfo == null) {
217           if (!sdk.suppressWarnings(
218               shadowClassElem, "robolectric.internal.IgnoreMissingClass", allowInDev)) {
219             problems.add("Shadowed type is not found: " + shadowedClassName, sdk.sdkInt);
220           }
221         } else {
222           StringBuilder builder = new StringBuilder();
223           helpers.appendParameterList(builder, shadowClassElem.getTypeParameters());
224           String shadowParams = builder.toString();
225           if (!classInfo.getSignature().equals(shadowParams)
226               && !sdk.suppressWarnings(shadowClassElem, "robolectric.mismatchedTypes", allowInDev)
227               && !looseSignatures) {
228             problems.add(
229                 "Shadow type is mismatched, expected "
230                     + shadowParams
231                     + " but found "
232                     + classInfo.getSignature(),
233                 sdk.sdkInt);
234           }
235         }
236       }
237     }
238     problems.recount(messager, shadowClassElem);
239     for (Element memberElement : ElementFilter.methodsIn(shadowClassElem.getEnclosedElements())) {
240       ExecutableElement methodElement = MoreElements.asExecutable(memberElement);
241 
242       // equals, hashCode, and toString are exempt, because of Robolectric's weird special behavior
243       if (METHODS_ALLOWED_TO_BE_PUBLIC.contains(methodElement.getSimpleName().toString())) {
244         continue;
245       }
246 
247       verifySdkMethod(shadowedClassName, methodElement, classMinSdk, classMaxSdk, looseSignatures);
248       if (shadowClassElem.getQualifiedName().toString().startsWith("org.robolectric")
249           && !methodElement.getModifiers().contains(Modifier.ABSTRACT)) {
250         checkForMissingImplementationAnnotation(
251             shadowedClassName, methodElement, classMinSdk, classMaxSdk, looseSignatures);
252       }
253 
254       String methodName = methodElement.getSimpleName().toString();
255       if (methodName.equals(CONSTRUCTOR_METHOD_NAME)
256           || methodName.equals(STATIC_INITIALIZER_METHOD_NAME)) {
257         Implementation implementation = memberElement.getAnnotation(Implementation.class);
258         if (implementation == null) {
259           messager.printMessage(
260               Kind.ERROR, "Shadow methods must be annotated @Implementation", methodElement);
261         }
262       }
263     }
264   }
265 
verifySdkMethod( String sdkClassName, ExecutableElement methodElement, int classMinSdk, int classMaxSdk, boolean looseSignatures)266   private void verifySdkMethod(
267       String sdkClassName,
268       ExecutableElement methodElement,
269       int classMinSdk,
270       int classMaxSdk,
271       boolean looseSignatures) {
272     if (sdkCheckMode == SdkCheckMode.OFF) {
273       return;
274     }
275 
276     Implementation implementation = methodElement.getAnnotation(Implementation.class);
277     if (implementation != null) {
278       Problems problems = new Problems(this.checkKind);
279 
280       for (SdkStore.Sdk sdk : sdkStore.sdksMatching(implementation, classMinSdk, classMaxSdk)) {
281         String problem = sdk.verifyMethod(sdkClassName, methodElement, looseSignatures, allowInDev);
282         if (problem != null) {
283           problems.add(problem, sdk.sdkInt);
284         }
285       }
286 
287       if (problems.any()) {
288         problems.recount(messager, methodElement);
289       }
290     }
291   }
292 
293   /**
294    * For the given {@link ExecutableElement}, check to see if it should have a {@link
295    * Implementation} tag but is missing one
296    */
checkForMissingImplementationAnnotation( String sdkClassName, ExecutableElement methodElement, int classMinSdk, int classMaxSdk, boolean looseSignatures)297   private void checkForMissingImplementationAnnotation(
298       String sdkClassName,
299       ExecutableElement methodElement,
300       int classMinSdk,
301       int classMaxSdk,
302       boolean looseSignatures) {
303 
304     if (sdkCheckMode == SdkCheckMode.OFF) {
305       return;
306     }
307 
308     Implementation implementation = methodElement.getAnnotation(Implementation.class);
309     if (implementation == null) {
310       Kind kind = sdkCheckMode == SdkCheckMode.WARN ? Kind.WARNING : Kind.ERROR;
311       Problems problems = new Problems(kind);
312 
313       for (SdkStore.Sdk sdk : sdkStore.sdksMatching(implementation, classMinSdk, classMaxSdk)) {
314         String problem = sdk.verifyMethod(sdkClassName, methodElement, looseSignatures, allowInDev);
315         if (problem == null && sdk.getClassInfo(sdkClassName) != null) {
316           problems.add(
317               "Missing @Implementation on method " + methodElement.getSimpleName(), sdk.sdkInt);
318         }
319       }
320 
321       if (problems.any()) {
322         problems.recount(messager, methodElement);
323       }
324     }
325   }
326 
captureJavadoc(TypeElement elem)327   private void captureJavadoc(TypeElement elem) {
328     List<String> imports = new ArrayList<>();
329     try {
330       List<? extends ImportTree> importLines =
331           Trees.instance(env).getPath(elem).getCompilationUnit().getImports();
332       for (ImportTree importLine : importLines) {
333         imports.add(importLine.getQualifiedIdentifier().toString());
334       }
335     } catch (IllegalArgumentException e) {
336       // Trees relies on javac APIs and is not available in all annotation processing
337       // implementations
338     }
339 
340     List<TypeElement> enclosedTypes = ElementFilter.typesIn(elem.getEnclosedElements());
341     for (TypeElement enclosedType : enclosedTypes) {
342       imports.add(enclosedType.getQualifiedName().toString());
343     }
344 
345     Elements elementUtils = env.getElementUtils();
346     modelBuilder.documentType(elem, elementUtils.getDocComment(elem), imports);
347 
348     for (Element memberElement : ElementFilter.methodsIn(elem.getEnclosedElements())) {
349       try {
350         ExecutableElement methodElement = (ExecutableElement) memberElement;
351         Implementation implementation = memberElement.getAnnotation(Implementation.class);
352 
353         DocumentedMethod documentedMethod = new DocumentedMethod(memberElement.toString());
354         for (Modifier modifier : memberElement.getModifiers()) {
355           documentedMethod.modifiers.add(modifier.toString());
356         }
357         documentedMethod.isImplementation = implementation != null;
358         if (implementation != null) {
359           documentedMethod.minSdk = sdkOrNull(implementation.minSdk());
360           documentedMethod.maxSdk = sdkOrNull(implementation.maxSdk());
361         }
362         for (VariableElement variableElement : methodElement.getParameters()) {
363           documentedMethod.params.add(variableElement.toString());
364         }
365         documentedMethod.returnType = methodElement.getReturnType().toString();
366         for (TypeMirror typeMirror : methodElement.getThrownTypes()) {
367           documentedMethod.exceptions.add(typeMirror.toString());
368         }
369         String docMd = elementUtils.getDocComment(methodElement);
370         if (docMd != null) {
371           documentedMethod.setDocumentation(docMd);
372         }
373 
374         modelBuilder.documentMethod(elem, documentedMethod);
375       } catch (Exception e) {
376         throw new RuntimeException(
377             "failed to capture javadoc for " + elem + "." + memberElement, e);
378       }
379     }
380   }
381 
sdkOrNull(int sdk)382   private Integer sdkOrNull(int sdk) {
383     return sdk == -1 ? null : sdk;
384   }
385 
386   private static class Problems {
387     private final Kind kind;
388     private final Map<String, Set<Integer>> problems = new HashMap<>();
389 
Problems(Kind kind)390     public Problems(Kind kind) {
391       this.kind = kind;
392     }
393 
add(String problem, int sdkInt)394     void add(String problem, int sdkInt) {
395       Set<Integer> sdks = problems.get(problem);
396       if (sdks == null) {
397         problems.put(problem, sdks = new TreeSet<>());
398       }
399       sdks.add(sdkInt);
400     }
401 
any()402     boolean any() {
403       return !problems.isEmpty();
404     }
405 
recount(Messager messager, Element element)406     void recount(Messager messager, Element element) {
407       for (Entry<String, Set<Integer>> e : problems.entrySet()) {
408         String problem = e.getKey();
409         Set<Integer> sdks = e.getValue();
410 
411         StringBuilder buf = new StringBuilder();
412         buf.append(problem).append(" for ").append(sdks.size() == 1 ? "SDK " : "SDKs ");
413 
414         Integer previousSdk = null;
415         Integer lastSdk = null;
416         for (Integer sdk : sdks) {
417           if (previousSdk == null) {
418             buf.append(sdk);
419           } else {
420             if (previousSdk != sdk - 1) {
421               buf.append("-").append(previousSdk);
422               buf.append("/").append(sdk);
423               lastSdk = null;
424             } else {
425               lastSdk = sdk;
426             }
427           }
428 
429           previousSdk = sdk;
430         }
431 
432         if (lastSdk != null) {
433           buf.append("-").append(lastSdk);
434         }
435 
436         messager.printMessage(kind, buf.toString(), element);
437       }
438     }
439   }
440 }
441