• 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.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 
ImplementsValidator(RobolectricModel.Builder modelBuilder, ProcessingEnvironment env, SdkCheckMode sdkCheckMode)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 
getClassNameTypeElement(AnnotationValue cv)67   private TypeElement getClassNameTypeElement(AnnotationValue cv) {
68     String className = Helpers.getAnnotationStringValue(cv);
69     return elements.getTypeElement(className.replace('$', '.'));
70   }
71 
72   @Override
visitType(TypeElement shadowType, Element parent)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 
addShadowNotInSdk(TypeElement shadowType, AnnotationValue av, AnnotationValue cv)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 
suppressWarnings(Element element, String warningName)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 
getClassFQName(TypeElement elem)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 
isClassy(ElementKind kind)203   private static boolean isClassy(ElementKind kind) {
204     return kind == ElementKind.CLASS || kind == ElementKind.INTERFACE;
205   }
206 
validateShadowMethods(TypeElement sdkClassElem, TypeElement shadowClassElem, int classMinSdk, int classMaxSdk, boolean looseSignatures)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 
verifySdkMethod(TypeElement sdkClassElem, ExecutableElement methodElement, int classMinSdk, int classMaxSdk, boolean looseSignatures)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 
captureJavadoc(TypeElement elem)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 
sdkOrNull(int sdk)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 
Problems(Kind kind)314     public Problems(Kind kind) {
315       this.kind = kind;
316     }
317 
add(String problem, int sdkInt)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 
any()326     boolean any() {
327       return !problems.isEmpty();
328     }
329 
recount(Messager messager, Element element)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