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