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