1 package org.robolectric.annotation.processing.validator; 2 3 import static org.robolectric.annotation.Implementation.DEFAULT_SDK; 4 import static org.robolectric.annotation.processing.validator.ImplementsValidator.CONSTRUCTOR_METHOD_NAME; 5 import static org.robolectric.annotation.processing.validator.ImplementsValidator.STATIC_INITIALIZER_METHOD_NAME; 6 import static org.robolectric.annotation.processing.validator.ImplementsValidator.getClassFQName; 7 8 import com.sun.tools.javac.code.Type.ArrayType; 9 import com.sun.tools.javac.code.Type.TypeVar; 10 import java.io.BufferedReader; 11 import java.io.File; 12 import java.io.FileOutputStream; 13 import java.io.IOException; 14 import java.io.InputStream; 15 import java.io.InputStreamReader; 16 import java.net.URI; 17 import java.nio.charset.Charset; 18 import java.util.ArrayList; 19 import java.util.HashMap; 20 import java.util.List; 21 import java.util.Map; 22 import java.util.Objects; 23 import java.util.Properties; 24 import java.util.Set; 25 import java.util.TreeSet; 26 import java.util.jar.JarFile; 27 import java.util.zip.ZipEntry; 28 import javax.lang.model.element.ExecutableElement; 29 import javax.lang.model.element.Modifier; 30 import javax.lang.model.element.TypeElement; 31 import javax.lang.model.element.VariableElement; 32 import javax.lang.model.type.TypeMirror; 33 import org.objectweb.asm.ClassReader; 34 import org.objectweb.asm.Opcodes; 35 import org.objectweb.asm.Type; 36 import org.objectweb.asm.tree.ClassNode; 37 import org.objectweb.asm.tree.MethodNode; 38 import org.robolectric.annotation.Implementation; 39 40 class SdkStore { 41 42 private final Set<Sdk> sdks = new TreeSet<>(); 43 private boolean loaded = false; 44 sdksMatching(Implementation implementation, int classMinSdk, int classMaxSdk)45 List<Sdk> sdksMatching(Implementation implementation, int classMinSdk, int classMaxSdk) { 46 loadSdksOnce(); 47 48 int minSdk = implementation == null ? DEFAULT_SDK : implementation.minSdk(); 49 if (minSdk == DEFAULT_SDK) { 50 minSdk = 0; 51 } 52 if (classMinSdk > minSdk) { 53 minSdk = classMinSdk; 54 } 55 56 int maxSdk = implementation == null ? -1 : implementation.maxSdk(); 57 if (maxSdk == -1) { 58 maxSdk = Integer.MAX_VALUE; 59 } 60 if (classMaxSdk != -1 && classMaxSdk < maxSdk) { 61 maxSdk = classMaxSdk; 62 } 63 64 List<Sdk> matchingSdks = new ArrayList<>(); 65 for (Sdk sdk : sdks) { 66 Integer sdkInt = sdk.sdkInt; 67 if (sdkInt >= minSdk && sdkInt <= maxSdk) { 68 matchingSdks.add(sdk); 69 } 70 } 71 return matchingSdks; 72 } 73 loadSdksOnce()74 private synchronized void loadSdksOnce() { 75 if (!loaded) { 76 sdks.addAll(loadFromSdksFile("/sdks.txt")); 77 loaded = true; 78 } 79 } 80 loadFromSdksFile(String resourceFileName)81 private static List<Sdk> loadFromSdksFile(String resourceFileName) { 82 try (InputStream resIn = SdkStore.class.getResourceAsStream(resourceFileName)) { 83 if (resIn == null) { 84 throw new RuntimeException("no such resource " + resourceFileName); 85 } 86 87 BufferedReader in = 88 new BufferedReader(new InputStreamReader(resIn, Charset.defaultCharset())); 89 List<Sdk> sdks = new ArrayList<>(); 90 String line; 91 while ((line = in.readLine()) != null) { 92 if (!line.startsWith("#")) { 93 sdks.add(new Sdk(line)); 94 } 95 } 96 return sdks; 97 } catch (IOException e) { 98 throw new RuntimeException("failed reading " + resourceFileName, e); 99 } 100 } 101 canonicalize(TypeMirror typeMirror)102 private static String canonicalize(TypeMirror typeMirror) { 103 if (typeMirror instanceof TypeVar) { 104 return ((TypeVar) typeMirror).getUpperBound().toString(); 105 } else if (typeMirror instanceof ArrayType) { 106 return canonicalize(((ArrayType) typeMirror).elemtype) + "[]"; 107 } else { 108 return typeMirror.toString(); 109 } 110 } 111 typeWithoutGenerics(String paramType)112 private static String typeWithoutGenerics(String paramType) { 113 return paramType.replaceAll("<.*", ""); 114 } 115 116 static class Sdk implements Comparable<Sdk> { 117 private static final ClassInfo NULL_CLASS_INFO = new ClassInfo(); 118 119 private final String path; 120 private final JarFile jarFile; 121 final int sdkInt; 122 private final Map<String, ClassInfo> classInfos = new HashMap<>(); 123 private static File tempDir; 124 Sdk(String path)125 Sdk(String path) { 126 this.path = path; 127 this.jarFile = ensureJar(); 128 this.sdkInt = readSdkInt(); 129 } 130 131 /** 132 * Matches an `@Implementation` method against the framework method for this SDK. 133 * 134 * @param sdkClassElem the framework class being shadowed 135 * @param methodElement the `@Implementation` method declaration to check 136 * @param looseSignatures if `true`, also match any framework method with the same class, 137 * name, return type, and arity of parameters. 138 * @return a string describing any problems with this method, or `null` if it checks out. 139 */ verifyMethod(TypeElement sdkClassElem, ExecutableElement methodElement, boolean looseSignatures)140 public String verifyMethod(TypeElement sdkClassElem, ExecutableElement methodElement, 141 boolean looseSignatures) { 142 String className = getClassFQName(sdkClassElem); 143 ClassInfo classInfo = getClassInfo(className); 144 145 if (classInfo == null) { 146 return "No such class " + className; 147 } 148 149 MethodExtraInfo sdkMethod = classInfo.findMethod(methodElement, looseSignatures); 150 if (sdkMethod == null) { 151 return "No such method in " + className; 152 } 153 154 MethodExtraInfo implMethod = new MethodExtraInfo(methodElement); 155 if (!sdkMethod.equals(implMethod) 156 && !suppressWarnings(methodElement, "robolectric.ShadowReturnTypeMismatch")) { 157 if (implMethod.isStatic != sdkMethod.isStatic) { 158 return "@Implementation for " + methodElement.getSimpleName() 159 + " is " + (implMethod.isStatic ? "static" : "not static") 160 + " unlike the SDK method"; 161 } 162 if (!implMethod.returnType.equals(sdkMethod.returnType)) { 163 if ( 164 (looseSignatures && typeIsOkForLooseSignatures(implMethod, sdkMethod)) 165 || (looseSignatures && implMethod.returnType.equals("java.lang.Object[]")) 166 // Number is allowed for int or long return types 167 || typeIsNumeric(sdkMethod, implMethod)) { 168 return null; 169 } else { 170 return "@Implementation for " + methodElement.getSimpleName() 171 + " has a return type of " + implMethod.returnType 172 + ", not " + sdkMethod.returnType + " as in the SDK method"; 173 } 174 } 175 } 176 177 return null; 178 } 179 suppressWarnings(ExecutableElement methodElement, String warningName)180 private boolean suppressWarnings(ExecutableElement methodElement, String warningName) { 181 SuppressWarnings[] suppressWarnings = methodElement.getAnnotationsByType(SuppressWarnings.class); 182 for (SuppressWarnings suppression : suppressWarnings) { 183 for (String name : suppression.value()) { 184 if (warningName.equals(name)) { 185 return true; 186 } 187 } 188 } 189 return false; 190 } 191 typeIsNumeric(MethodExtraInfo sdkMethod, MethodExtraInfo implMethod)192 private boolean typeIsNumeric(MethodExtraInfo sdkMethod, MethodExtraInfo implMethod) { 193 return implMethod.returnType.equals("java.lang.Number") 194 && isNumericType(sdkMethod.returnType); 195 } 196 typeIsOkForLooseSignatures(MethodExtraInfo implMethod, MethodExtraInfo sdkMethod)197 private boolean typeIsOkForLooseSignatures(MethodExtraInfo implMethod, MethodExtraInfo sdkMethod) { 198 return 199 // loose signatures allow a return type of Object... 200 implMethod.returnType.equals("java.lang.Object") 201 // or Object[] for arrays... 202 || (implMethod.returnType.equals("java.lang.Object[]") 203 && sdkMethod.returnType.endsWith("[]")); 204 } 205 isNumericType(String type)206 private boolean isNumericType(String type) { 207 return type.equals("int") || type.equals("long"); 208 } 209 210 /** 211 * Load and analyze bytecode for the specified class, with caching. 212 * 213 * @param name the name of the class to analyze 214 * @return information about the methods in the specified class 215 */ getClassInfo(String name)216 private synchronized ClassInfo getClassInfo(String name) { 217 ClassInfo classInfo = classInfos.get(name); 218 if (classInfo == null) { 219 ClassNode classNode = loadClassNode(name); 220 221 if (classNode == null) { 222 classInfos.put(name, NULL_CLASS_INFO); 223 } else { 224 classInfo = new ClassInfo(classNode); 225 classInfos.put(name, classInfo); 226 } 227 } 228 229 return classInfo == NULL_CLASS_INFO ? null : classInfo; 230 } 231 232 /** 233 * Determine the API level for this SDK jar by inspecting its `build.prop` file. 234 * 235 * If the `ro.build.version.codename` value isn't `REL`, this is an unreleased SDK, which 236 * is represented as `10000` (see {@link android.os.Build.VERSION_CODES#CUR_DEVELOPMENT}. 237 * 238 * @return the API level, or `10000` 239 */ readSdkInt()240 private int readSdkInt() { 241 Properties properties = new Properties(); 242 try (InputStream inputStream = jarFile.getInputStream(jarFile.getJarEntry("build.prop"))) { 243 properties.load(inputStream); 244 } catch (IOException e) { 245 throw new RuntimeException("failed to read build.prop from " + path); 246 } 247 int sdkInt = Integer.parseInt(properties.getProperty("ro.build.version.sdk")); 248 String codename = properties.getProperty("ro.build.version.codename"); 249 if (!"REL".equals(codename)) { 250 sdkInt = 10000; 251 } 252 253 return sdkInt; 254 } 255 ensureJar()256 private JarFile ensureJar() { 257 try { 258 if (path.startsWith("classpath:")) { 259 return new JarFile(copyResourceToFile(URI.create(path).getSchemeSpecificPart())); 260 } else { 261 return new JarFile(path); 262 } 263 264 } catch (IOException e) { 265 throw new RuntimeException("failed to open SDK " + sdkInt + " at " + path, e); 266 } 267 } 268 copyResourceToFile(String resourcePath)269 private static File copyResourceToFile(String resourcePath) throws IOException { 270 if (tempDir == null){ 271 File tempFile = File.createTempFile("prefix", "suffix"); 272 tempFile.deleteOnExit(); 273 tempDir = tempFile.getParentFile(); 274 } 275 InputStream jarIn = SdkStore.class.getClassLoader().getResourceAsStream(resourcePath); 276 File outFile = new File(tempDir, new File(resourcePath).getName()); 277 outFile.deleteOnExit(); 278 try (FileOutputStream jarOut = new FileOutputStream(outFile)) { 279 byte[] buffer = new byte[4096]; 280 int len; 281 while ((len = jarIn.read(buffer)) != -1) { 282 jarOut.write(buffer, 0, len); 283 } 284 } 285 286 return outFile; 287 } 288 loadClassNode(String name)289 private ClassNode loadClassNode(String name) { 290 String classFileName = name.replace('.', '/') + ".class"; 291 ZipEntry entry = jarFile.getEntry(classFileName); 292 if (entry == null) { 293 return null; 294 } 295 try (InputStream inputStream = jarFile.getInputStream(entry)) { 296 ClassReader classReader = new ClassReader(inputStream); 297 ClassNode classNode = new ClassNode(); 298 classReader.accept(classNode, 299 ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); 300 return classNode; 301 } catch (IOException e) { 302 throw new RuntimeException("failed to analyze " + classFileName + " in " + path, e); 303 } 304 } 305 306 @Override compareTo(Sdk sdk)307 public int compareTo(Sdk sdk) { 308 return sdk.sdkInt - sdkInt; 309 } 310 } 311 312 static class ClassInfo { 313 private final Map<MethodInfo, MethodExtraInfo> methods = new HashMap<>(); 314 private final Map<MethodInfo, MethodExtraInfo> erasedParamTypesMethods = new HashMap<>(); 315 ClassInfo()316 private ClassInfo() { 317 } 318 ClassInfo(ClassNode classNode)319 public ClassInfo(ClassNode classNode) { 320 for (Object aMethod : classNode.methods) { 321 MethodNode method = ((MethodNode) aMethod); 322 MethodInfo methodInfo = new MethodInfo(method); 323 MethodExtraInfo methodExtraInfo = new MethodExtraInfo(method); 324 methods.put(methodInfo, methodExtraInfo); 325 erasedParamTypesMethods.put(methodInfo.erase(), methodExtraInfo); 326 } 327 } 328 findMethod(ExecutableElement methodElement, boolean looseSignatures)329 MethodExtraInfo findMethod(ExecutableElement methodElement, boolean looseSignatures) { 330 MethodInfo methodInfo = new MethodInfo(methodElement); 331 332 MethodExtraInfo methodExtraInfo = methods.get(methodInfo); 333 if (looseSignatures && methodExtraInfo == null) { 334 methodExtraInfo = erasedParamTypesMethods.get(methodInfo.erase()); 335 } 336 return methodExtraInfo; 337 } 338 } 339 340 static class MethodInfo { 341 private final String name; 342 private final List<String> paramTypes = new ArrayList<>(); 343 344 /** Create a MethodInfo from ASM in-memory representation (an Android framework method). */ MethodInfo(MethodNode method)345 public MethodInfo(MethodNode method) { 346 this.name = method.name; 347 for (Type type : Type.getArgumentTypes(method.desc)) { 348 paramTypes.add(normalize(type)); 349 } 350 } 351 352 /** Create a MethodInfo with all Object params (for looseSignatures=true). */ MethodInfo(String name, int size)353 public MethodInfo(String name, int size) { 354 this.name = name; 355 for (int i = 0; i < size; i++) { 356 paramTypes.add("java.lang.Object"); 357 } 358 } 359 360 /** Create a MethodInfo from AST (an @Implementation method in a shadow class). */ MethodInfo(ExecutableElement methodElement)361 public MethodInfo(ExecutableElement methodElement) { 362 this.name = cleanMethodName(methodElement); 363 364 for (VariableElement variableElement : methodElement.getParameters()) { 365 TypeMirror varTypeMirror = variableElement.asType(); 366 String paramType = canonicalize(varTypeMirror); 367 String paramTypeWithoutGenerics = typeWithoutGenerics(paramType); 368 paramTypes.add(paramTypeWithoutGenerics); 369 } 370 } 371 cleanMethodName(ExecutableElement methodElement)372 private String cleanMethodName(ExecutableElement methodElement) { 373 String name = methodElement.getSimpleName().toString(); 374 if (CONSTRUCTOR_METHOD_NAME.equals(name)) { 375 return "<init>"; 376 } else if (STATIC_INITIALIZER_METHOD_NAME.equals(name)) { 377 return "<clinit>"; 378 } else { 379 return name; 380 } 381 } 382 erase()383 public MethodInfo erase() { 384 return new MethodInfo(name, paramTypes.size()); 385 } 386 387 @Override equals(Object o)388 public boolean equals(Object o) { 389 if (this == o) { 390 return true; 391 } 392 if (o == null || getClass() != o.getClass()) { 393 return false; 394 } 395 MethodInfo that = (MethodInfo) o; 396 return Objects.equals(name, that.name) 397 && Objects.equals(paramTypes, that.paramTypes); 398 } 399 400 @Override hashCode()401 public int hashCode() { 402 return Objects.hash(name, paramTypes); 403 } 404 @Override toString()405 public String toString() { 406 return "MethodInfo{" 407 + "name='" + name + '\'' 408 + ", paramTypes=" + paramTypes 409 + '}'; 410 } 411 } 412 normalize(Type type)413 private static String normalize(Type type) { 414 return type.getClassName().replace('$', '.'); 415 } 416 417 static class MethodExtraInfo { 418 private final boolean isStatic; 419 private final String returnType; 420 MethodExtraInfo(MethodNode method)421 public MethodExtraInfo(MethodNode method) { 422 this.isStatic = (method.access & Opcodes.ACC_STATIC) != 0; 423 this.returnType = typeWithoutGenerics(normalize(Type.getReturnType(method.desc))); 424 } 425 MethodExtraInfo(ExecutableElement methodElement)426 public MethodExtraInfo(ExecutableElement methodElement) { 427 this.isStatic = methodElement.getModifiers().contains(Modifier.STATIC); 428 this.returnType = typeWithoutGenerics(canonicalize(methodElement.getReturnType())); 429 } 430 431 @Override equals(Object o)432 public boolean equals(Object o) { 433 if (this == o) { 434 return true; 435 } 436 if (o == null || getClass() != o.getClass()) { 437 return false; 438 } 439 MethodExtraInfo that = (MethodExtraInfo) o; 440 return isStatic == that.isStatic && 441 Objects.equals(returnType, that.returnType); 442 } 443 444 @Override hashCode()445 public int hashCode() { 446 return Objects.hash(isStatic, returnType); 447 } 448 } 449 } 450