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 7 import com.google.common.collect.ImmutableList; 8 import java.io.BufferedReader; 9 import java.io.File; 10 import java.io.FileInputStream; 11 import java.io.FileOutputStream; 12 import java.io.IOException; 13 import java.io.InputStream; 14 import java.io.InputStreamReader; 15 import java.net.URI; 16 import java.nio.charset.Charset; 17 import java.nio.file.Files; 18 import java.nio.file.Path; 19 import java.nio.file.Paths; 20 import java.util.ArrayList; 21 import java.util.Arrays; 22 import java.util.HashMap; 23 import java.util.List; 24 import java.util.Map; 25 import java.util.Objects; 26 import java.util.Set; 27 import java.util.TreeSet; 28 import java.util.function.Supplier; 29 import java.util.jar.JarFile; 30 import java.util.regex.Matcher; 31 import java.util.regex.Pattern; 32 import java.util.stream.Collectors; 33 import java.util.zip.ZipEntry; 34 import javax.lang.model.element.AnnotationMirror; 35 import javax.lang.model.element.AnnotationValue; 36 import javax.lang.model.element.Element; 37 import javax.lang.model.element.ExecutableElement; 38 import javax.lang.model.element.Modifier; 39 import javax.lang.model.element.VariableElement; 40 import javax.lang.model.type.ArrayType; 41 import javax.lang.model.type.TypeMirror; 42 import javax.lang.model.type.TypeVariable; 43 import org.objectweb.asm.ClassReader; 44 import org.objectweb.asm.Opcodes; 45 import org.objectweb.asm.Type; 46 import org.objectweb.asm.signature.SignatureReader; 47 import org.objectweb.asm.tree.ClassNode; 48 import org.objectweb.asm.tree.MethodNode; 49 import org.objectweb.asm.util.TraceSignatureVisitor; 50 import org.robolectric.annotation.ClassName; 51 import org.robolectric.annotation.Implementation; 52 import org.robolectric.annotation.InDevelopment; 53 import org.robolectric.versioning.AndroidVersionInitTools; 54 import org.robolectric.versioning.AndroidVersions; 55 56 /** Encapsulates a collection of Android framework jars. */ 57 public class SdkStore { 58 59 private static final String VALID_CLASS_NAME_ANNOTATION_CHARS = "^[a-zA-Z0-9_$.;\\[\\]]+$"; 60 61 private final Set<Sdk> sdks = new TreeSet<>(); 62 private boolean loaded = false; 63 64 /** Should only ever be needed for android platform development */ 65 private final boolean loadFromClasspath; 66 67 private final String overrideSdkLocation; 68 private final int overrideSdkInt; 69 private final String sdksFile; 70 71 /** */ SdkStore( String sdksFile, boolean loadFromClasspath, String overrideSdkLocation, int overrideSdkInt)72 public SdkStore( 73 String sdksFile, boolean loadFromClasspath, String overrideSdkLocation, int overrideSdkInt) { 74 this.sdksFile = sdksFile; 75 this.loadFromClasspath = loadFromClasspath; 76 this.overrideSdkLocation = overrideSdkLocation; 77 this.overrideSdkInt = overrideSdkInt; 78 } 79 80 /** 81 * Used to look up matching sdks for a declared shadow class. Needed to then find the class from 82 * the underlying sdks for comparison in the ImplementsValidator. 83 */ sdksMatching(int classMinSdk, int classMaxSdk)84 List<Sdk> sdksMatching(int classMinSdk, int classMaxSdk) { 85 loadSdksOnce(); 86 List<Sdk> matchingSdks = new ArrayList<>(); 87 for (Sdk sdk : sdks) { 88 int sdkInt = sdk.sdkRelease.getSdkInt(); 89 if (sdkInt >= classMinSdk && (sdkInt <= classMaxSdk || classMaxSdk == -1)) { 90 matchingSdks.add(sdk); 91 } 92 } 93 return matchingSdks; 94 } 95 sdksMatching(Implementation implementation, int classMinSdk, int classMaxSdk)96 List<Sdk> sdksMatching(Implementation implementation, int classMinSdk, int classMaxSdk) { 97 loadSdksOnce(); 98 99 int minSdk = implementation == null ? DEFAULT_SDK : implementation.minSdk(); 100 if (minSdk == DEFAULT_SDK) { 101 minSdk = 0; 102 } 103 if (classMinSdk > minSdk) { 104 minSdk = classMinSdk; 105 } 106 107 int maxSdk = implementation == null ? -1 : implementation.maxSdk(); 108 if (maxSdk == -1) { 109 maxSdk = Integer.MAX_VALUE; 110 } 111 if (classMaxSdk != -1 && classMaxSdk < maxSdk) { 112 maxSdk = classMaxSdk; 113 } 114 115 List<Sdk> matchingSdks = new ArrayList<>(); 116 for (Sdk sdk : sdks) { 117 int sdkInt = sdk.sdkRelease.getSdkInt(); 118 if (sdkInt >= minSdk && sdkInt <= maxSdk) { 119 matchingSdks.add(sdk); 120 } 121 } 122 return matchingSdks; 123 } 124 loadSdksOnce()125 private synchronized void loadSdksOnce() { 126 if (!loaded) { 127 sdks.addAll( 128 loadFromSources(loadFromClasspath, sdksFile, overrideSdkLocation, overrideSdkInt)); 129 loaded = true; 130 } 131 } 132 133 /** 134 * @return a list of sdk_int's to jar locations as a string, one tuple per line. 135 */ 136 @Override 137 @SuppressWarnings("JdkCollectors") toString()138 public String toString() { 139 loadSdksOnce(); 140 StringBuilder builder = new StringBuilder(); 141 builder.append("SdkStore ["); 142 for (Sdk sdk : sdks.stream().sorted().collect(Collectors.toList())) { 143 builder.append(" " + sdk.sdkRelease.getSdkInt() + " : " + sdk.path + "\n"); 144 } 145 builder.append("]"); 146 return builder.toString(); 147 } 148 149 /** 150 * Scans the jvm properties for the command that executed it, in this command will be the 151 * classpath. <br> 152 * <br> 153 * Scans all jars on the classpath for the first one with a /build.prop on resource. This is 154 * assumed to be the sdk that the processor is running with. 155 * 156 * @return the detected sdk location. 157 */ compilationSdkTarget()158 private static String compilationSdkTarget() { 159 String cmd = System.getProperty("sun.java.command"); 160 Pattern pattern = Pattern.compile("((-cp)|(-classpath))\\s(?<cp>[a-zA-Z-_0-9\\-\\:\\/\\.]*)"); 161 Matcher matcher = pattern.matcher(cmd); 162 if (matcher.find()) { 163 String classpathString = matcher.group("cp"); 164 List<String> cp = Arrays.asList(classpathString.split(":")); 165 for (String fileStr : cp) { 166 try (JarFile jarFile = new JarFile(fileStr)) { 167 ZipEntry entry = jarFile.getEntry("build.prop"); 168 if (entry != null) { 169 return fileStr; 170 } 171 } catch (IOException ioe) { 172 System.out.println("Error detecting compilation SDK: " + ioe.getMessage()); 173 ioe.printStackTrace(); 174 } 175 } 176 } 177 return null; 178 } 179 180 /** 181 * Returns a list of sdks to process, either the compilation's classpaths sdk in a list of size 182 * one, or the list of sdks in a sdkFile. This should not be needed unless building in the android 183 * codebase. Otherwise, should prefer using the sdks.txt and the released jars. 184 * 185 * @param localSdk validate sdk found in compile time classpath, takes precedence over sdkFile 186 * @param sdkFileName the sdkFile name, may be null, or empty 187 * @param overrideSdkLocation if provided overrides the default lookup of the localSdk, iff 188 * localSdk is on. 189 * @return a list of sdks to check with annotation processing validators. 190 */ loadFromSources( boolean localSdk, String sdkFileName, String overrideSdkLocation, int overrideSdkInt)191 private static ImmutableList<Sdk> loadFromSources( 192 boolean localSdk, String sdkFileName, String overrideSdkLocation, int overrideSdkInt) { 193 if (localSdk) { 194 Sdk sdk = null; 195 if (overrideSdkLocation != null) { 196 sdk = new Sdk(overrideSdkLocation, overrideSdkInt); 197 return sdk == null ? ImmutableList.of() : ImmutableList.of(sdk); 198 } else { 199 String target = compilationSdkTarget(); 200 if (target != null) { 201 sdk = new Sdk(target); 202 // We don't want to test released versions in Android source tree. 203 return sdk == null || sdk.sdkRelease.isReleased() 204 ? ImmutableList.of() 205 : ImmutableList.of(sdk); 206 } 207 } 208 } 209 if (sdkFileName == null || Files.notExists(Paths.get(sdkFileName))) { 210 return ImmutableList.of(); 211 } 212 try (InputStream resIn = new FileInputStream(sdkFileName)) { 213 if (resIn == null) { 214 throw new RuntimeException("no such file " + sdkFileName); 215 } 216 BufferedReader in = 217 new BufferedReader(new InputStreamReader(resIn, Charset.defaultCharset())); 218 List<Sdk> sdks = new ArrayList<>(); 219 String line; 220 while ((line = in.readLine()) != null) { 221 if (!line.startsWith("#")) { 222 sdks.add(new Sdk(line)); 223 } 224 } 225 return ImmutableList.copyOf(sdks); 226 } catch (IOException e) { 227 throw new RuntimeException("failed reading " + sdkFileName, e); 228 } 229 } 230 canonicalize(TypeMirror typeMirror)231 private static String canonicalize(TypeMirror typeMirror) { 232 if (typeMirror instanceof TypeVariable) { 233 return ((TypeVariable) typeMirror).getUpperBound().toString(); 234 } else if (typeMirror instanceof ArrayType) { 235 return canonicalize(((ArrayType) typeMirror).getComponentType()) + "[]"; 236 } else { 237 return typeMirror.toString(); 238 } 239 } 240 typeWithoutGenerics(String paramType)241 private static String typeWithoutGenerics(String paramType) { 242 return paramType.replaceAll("<.*", ""); 243 } 244 245 static class Sdk implements Comparable<Sdk> { 246 private static final ClassInfo NULL_CLASS_INFO = new ClassInfo(); 247 248 private final String path; 249 private final JarFile jarFile; 250 final AndroidVersions.AndroidRelease sdkRelease; 251 final int sdkInt; 252 private final Map<String, ClassInfo> classInfos = new HashMap<>(); 253 private static File tempDir; 254 Sdk(String path)255 Sdk(String path) { 256 this(path, null); 257 } 258 Sdk(String path, Integer sdkInt)259 Sdk(String path, Integer sdkInt) { 260 this.path = path; 261 if (path.startsWith("classpath:") || path.endsWith(".jar")) { 262 this.jarFile = ensureJar(); 263 } else { 264 this.jarFile = null; 265 } 266 if (sdkInt == null) { 267 this.sdkRelease = readSdkVersion(); 268 this.sdkInt = sdkRelease.getSdkInt(); 269 } else { 270 this.sdkRelease = AndroidVersions.getReleaseForSdkInt(sdkInt); 271 this.sdkInt = sdkRelease.getSdkInt(); 272 } 273 } 274 275 /** 276 * Matches an {@code @Implementation} method against the framework method for this SDK. 277 * 278 * @param sdkClassName the framework class being shadowed 279 * @param methodElement the {@code @Implementation} method declaration to check 280 * @param looseSignatures if true, also match any framework method with the same class, name, 281 * return type, and arity of parameters. 282 * @return a string describing any problems with this method, or null if it checks out. 283 */ verifyMethod( String sdkClassName, ExecutableElement methodElement, boolean looseSignatures, boolean allowInDev)284 public String verifyMethod( 285 String sdkClassName, 286 ExecutableElement methodElement, 287 boolean looseSignatures, 288 boolean allowInDev) { 289 ClassInfo classInfo = getClassInfo(sdkClassName); 290 291 // Probably should not be reachable 292 if (classInfo == null 293 && !suppressWarnings(methodElement.getEnclosingElement(), null, allowInDev)) { 294 return null; 295 } 296 297 MethodExtraInfo sdkMethod = classInfo.findMethod(methodElement, looseSignatures); 298 if (sdkMethod == null && !suppressWarnings(methodElement, null, allowInDev)) { 299 return "No method " + methodElement + " in " + sdkClassName; 300 } 301 if (sdkMethod != null) { 302 MethodExtraInfo implMethod = new MethodExtraInfo(methodElement); 303 if (!sdkMethod.equals(implMethod) 304 && !suppressWarnings( 305 methodElement, "robolectric.ShadowReturnTypeMismatch", allowInDev)) { 306 if (implMethod.isStatic != sdkMethod.isStatic) { 307 return "@Implementation for " 308 + methodElement.getSimpleName() 309 + " is " 310 + (implMethod.isStatic ? "static" : "not static") 311 + " unlike the SDK method"; 312 } 313 if (!implMethod.returnType.equals(sdkMethod.returnType)) { 314 if ((looseSignatures && typeIsOkForLooseSignatures(implMethod, sdkMethod)) 315 || (looseSignatures && implMethod.returnType.equals("java.lang.Object[]"))) { 316 return null; 317 } else { 318 return "@Implementation for " 319 + methodElement.getSimpleName() 320 + " has a return type of " 321 + implMethod.returnType 322 + ", not " 323 + sdkMethod.returnType 324 + " as in the SDK method"; 325 } 326 } 327 } 328 } 329 330 return null; 331 } 332 333 /** 334 * Warnings (or potentially Errors, depending on processing flags) can be suppressed in one of 335 * two ways, either with @SuppressWarnings("robolectric.<warningName>"), or with 336 * the @InDevelopment annotation, if and only the target Sdk is in development. 337 * 338 * @param annotatedElement element to inspect for annotations 339 * @param warningName the name of the warning, if null, @InDevelopment will still be honored. 340 * @return true if the warning should be suppressed, else false 341 */ suppressWarnings(Element annotatedElement, String warningName, boolean allowInDev)342 boolean suppressWarnings(Element annotatedElement, String warningName, boolean allowInDev) { 343 SuppressWarnings[] suppressWarnings = 344 annotatedElement.getAnnotationsByType(SuppressWarnings.class); 345 for (SuppressWarnings suppression : suppressWarnings) { 346 for (String name : suppression.value()) { 347 if (warningName != null && warningName.equals(name)) { 348 return true; 349 } 350 } 351 } 352 InDevelopment[] inDev = annotatedElement.getAnnotationsByType(InDevelopment.class); 353 // Marked in development, sdk is not released, or is the last release (which may still be 354 // marked unreleased in g/main aosp/main. 355 if (allowInDev 356 && inDev.length > 0 357 && (!sdkRelease.isReleased() 358 || sdkRelease 359 == AndroidVersions.getReleases().stream() 360 .max(AndroidVersions.AndroidRelease::compareTo) 361 .get())) { 362 return true; 363 } 364 return false; 365 } 366 typeIsOkForLooseSignatures( MethodExtraInfo implMethod, MethodExtraInfo sdkMethod)367 private static boolean typeIsOkForLooseSignatures( 368 MethodExtraInfo implMethod, MethodExtraInfo sdkMethod) { 369 return 370 // loose signatures allow a return type of Object... 371 implMethod.returnType.equals("java.lang.Object") 372 // or Object[] for arrays... 373 || (implMethod.returnType.equals("java.lang.Object[]") 374 && sdkMethod.returnType.endsWith("[]")); 375 } 376 377 /** 378 * Load and analyze bytecode for the specified class, with caching. 379 * 380 * @param name the name of the class to analyze 381 * @return information about the methods in the specified class 382 */ getClassInfo(String name)383 synchronized ClassInfo getClassInfo(String name) { 384 ClassInfo classInfo = classInfos.get(name); 385 if (classInfo == null) { 386 ClassNode classNode = loadClassNode(name); 387 388 if (classNode == null) { 389 classInfos.put(name, NULL_CLASS_INFO); 390 } else { 391 classInfo = new ClassInfo(classNode); 392 classInfos.put(name, classInfo); 393 } 394 } 395 396 return classInfo == NULL_CLASS_INFO ? null : classInfo; 397 } 398 399 /** 400 * Determine the API level for this SDK jar by inspecting its {@code build.prop} file. 401 * 402 * @return the API level 403 */ readSdkVersion()404 private AndroidVersions.AndroidRelease readSdkVersion() { 405 try { 406 return AndroidVersionInitTools.computeReleaseVersion(jarFile); 407 } catch (IOException e) { 408 throw new RuntimeException("failed to read build.prop from " + path); 409 } 410 } 411 ensureJar()412 private JarFile ensureJar() { 413 try { 414 if (path.startsWith("classpath:")) { 415 return new JarFile(copyResourceToFile(URI.create(path).getSchemeSpecificPart())); 416 } else { 417 return new JarFile(path); 418 } 419 420 } catch (IOException e) { 421 throw new RuntimeException( 422 "failed to open SDK " + sdkRelease.getSdkInt() + " at " + path, e); 423 } 424 } 425 copyResourceToFile(String resourcePath)426 private static File copyResourceToFile(String resourcePath) throws IOException { 427 if (tempDir == null) { 428 File tempFile = File.createTempFile("prefix", "suffix"); 429 tempFile.deleteOnExit(); 430 tempDir = tempFile.getParentFile(); 431 } 432 try (InputStream jarIn = SdkStore.class.getClassLoader().getResourceAsStream(resourcePath)) { 433 if (jarIn == null) { 434 throw new RuntimeException("SDK " + resourcePath + " not found"); 435 } 436 File outFile = new File(tempDir, new File(resourcePath).getName()); 437 outFile.deleteOnExit(); 438 try (FileOutputStream jarOut = new FileOutputStream(outFile)) { 439 byte[] buffer = new byte[4096]; 440 int len; 441 while ((len = jarIn.read(buffer)) != -1) { 442 jarOut.write(buffer, 0, len); 443 } 444 } 445 446 return outFile; 447 } 448 } 449 loadClassNode(String name)450 private ClassNode loadClassNode(String name) { 451 String classFileName = name.replace('.', '/') + ".class"; 452 Supplier<InputStream> inputStreamSupplier = null; 453 454 if (jarFile != null) { 455 // working with a jar file. 456 ZipEntry entry = jarFile.getEntry(classFileName); 457 if (entry == null) { 458 return null; 459 } 460 inputStreamSupplier = 461 () -> { 462 try { 463 return jarFile.getInputStream(entry); 464 } catch (IOException ioe) { 465 throw new RuntimeException("could not read zip entry", ioe); 466 } 467 }; 468 } else { 469 // working with an exploded path location. 470 Path working = Path.of(path, classFileName); 471 File classFile = working.toFile(); 472 if (classFile.isFile()) { 473 inputStreamSupplier = 474 () -> { 475 try { 476 return new FileInputStream(classFile); 477 } catch (IOException ioe) { 478 throw new RuntimeException("could not read file in path " + working, ioe); 479 } 480 }; 481 } 482 } 483 if (inputStreamSupplier == null) { 484 return null; 485 } 486 try (InputStream inputStream = inputStreamSupplier.get()) { 487 ClassReader classReader = new ClassReader(inputStream); 488 ClassNode classNode = new ClassNode(); 489 classReader.accept( 490 classNode, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); 491 return classNode; 492 } catch (IOException e) { 493 throw new RuntimeException("failed to analyze " + classFileName + " in " + path, e); 494 } 495 } 496 497 @Override compareTo(Sdk sdk)498 public int compareTo(Sdk sdk) { 499 return sdk.sdkRelease.getSdkInt() - sdkRelease.getSdkInt(); 500 } 501 } 502 503 static class ClassInfo { 504 private final Map<MethodInfo, MethodExtraInfo> methods = new HashMap<>(); 505 private final Map<MethodInfo, MethodExtraInfo> erasedParamTypesMethods = new HashMap<>(); 506 private final String signature; 507 ClassInfo()508 private ClassInfo() { 509 signature = ""; 510 } 511 ClassInfo(ClassNode classNode)512 public ClassInfo(ClassNode classNode) { 513 if (classNode.signature != null) { 514 TraceSignatureVisitor signatureVisitor = new TraceSignatureVisitor(0); 515 new SignatureReader(classNode.signature).accept(signatureVisitor); 516 signature = stripExtends(signatureVisitor.getDeclaration()); 517 } else { 518 signature = ""; 519 } 520 for (Object aMethod : classNode.methods) { 521 MethodNode method = ((MethodNode) aMethod); 522 MethodInfo methodInfo = new MethodInfo(method); 523 MethodExtraInfo methodExtraInfo = new MethodExtraInfo(method); 524 methods.put(methodInfo, methodExtraInfo); 525 erasedParamTypesMethods.put(methodInfo.erase(), methodExtraInfo); 526 } 527 } 528 529 /** 530 * In order to compare typeMirror derived strings of Type parameters, ie `{@code Clazz<X extends 531 * Y>}` from a class definition, with a asm bytecode read string of the same, any extends info 532 * is not supplied by type parameters, but is by asm class readers `{@code Clazz<X extends Y> 533 * extends Clazz1}`. 534 * 535 * <p>This method can strip any extra information `{@code extends Clazz1}`, from a Generics type 536 * parameter string provided by asm byte code readers. 537 */ stripExtends(String asmTypeSuffix)538 private static String stripExtends(String asmTypeSuffix) { 539 int count = 0; 540 for (int loc = 0; loc < asmTypeSuffix.length(); loc++) { 541 char c = asmTypeSuffix.charAt(loc); 542 if (c == '<') { 543 count += 1; 544 } else if (c == '>') { 545 count -= 1; 546 } 547 if (count == 0) { 548 return asmTypeSuffix.substring(0, loc + 1).trim(); 549 } 550 } 551 return ""; 552 } 553 findMethod(ExecutableElement methodElement, boolean looseSignatures)554 MethodExtraInfo findMethod(ExecutableElement methodElement, boolean looseSignatures) { 555 MethodInfo methodInfo = new MethodInfo(methodElement); 556 557 MethodExtraInfo methodExtraInfo = methods.get(methodInfo); 558 if (looseSignatures && methodExtraInfo == null) { 559 methodExtraInfo = erasedParamTypesMethods.get(methodInfo); 560 } 561 return methodExtraInfo; 562 } 563 getSignature()564 String getSignature() { 565 return signature; 566 } 567 } 568 569 static class MethodInfo { 570 private final String name; 571 private final List<String> paramTypes = new ArrayList<>(); 572 573 /** Create a MethodInfo from ASM in-memory representation (an Android framework method). */ MethodInfo(MethodNode method)574 public MethodInfo(MethodNode method) { 575 this.name = method.name; 576 for (Type type : Type.getArgumentTypes(method.desc)) { 577 paramTypes.add(normalize(type)); 578 } 579 } 580 581 /** Create a MethodInfo with all Object params (for looseSignatures=true). */ MethodInfo(String name, int size)582 public MethodInfo(String name, int size) { 583 this.name = name; 584 for (int i = 0; i < size; i++) { 585 paramTypes.add("java.lang.Object"); 586 } 587 } 588 589 /** Create a MethodInfo from AST (an @Implementation method in a shadow class). */ MethodInfo(ExecutableElement methodElement)590 public MethodInfo(ExecutableElement methodElement) { 591 this.name = cleanMethodName(methodElement); 592 593 for (VariableElement variableElement : methodElement.getParameters()) { 594 TypeMirror varTypeMirror = variableElement.asType(); 595 String paramType = canonicalize(varTypeMirror); 596 597 // If parameter is annotated with @ClassName, then use the indicated type instead. 598 ClassName className = variableElement.getAnnotation(ClassName.class); 599 if (className != null) { 600 if (!className.value().matches(VALID_CLASS_NAME_ANNOTATION_CHARS)) { 601 throw new RuntimeException( 602 "Invalid @ClassName annotation '" 603 + paramType 604 + "' in " 605 + methodElement.getEnclosingElement().getSimpleName() 606 + "." 607 + methodElement.getSimpleName()); 608 } 609 paramType = className.value().replace('$', '.'); 610 } 611 612 String paramTypeWithoutGenerics = typeWithoutGenerics(paramType); 613 paramTypes.add(paramTypeWithoutGenerics); 614 } 615 } 616 cleanMethodName(ExecutableElement methodElement)617 private static String cleanMethodName(ExecutableElement methodElement) { 618 String name = methodElement.getSimpleName().toString(); 619 if (CONSTRUCTOR_METHOD_NAME.equals(name)) { 620 return "<init>"; 621 } else if (STATIC_INITIALIZER_METHOD_NAME.equals(name)) { 622 return "<clinit>"; 623 } else { 624 Implementation implementation = methodElement.getAnnotation(Implementation.class); 625 String methodName = implementation == null ? "" : implementation.methodName(); 626 methodName = methodName == null ? "" : methodName.trim(); 627 if (methodName.isEmpty()) { 628 return name; 629 } else { 630 return methodName; 631 } 632 } 633 } 634 erase()635 public MethodInfo erase() { 636 return new MethodInfo(name, paramTypes.size()); 637 } 638 639 @Override equals(Object o)640 public boolean equals(Object o) { 641 if (this == o) { 642 return true; 643 } 644 if (!(o instanceof MethodInfo)) { 645 return false; 646 } 647 MethodInfo that = (MethodInfo) o; 648 return Objects.equals(name, that.name) && Objects.equals(paramTypes, that.paramTypes); 649 } 650 651 @Override hashCode()652 public int hashCode() { 653 return Objects.hash(name, paramTypes); 654 } 655 656 @Override toString()657 public String toString() { 658 return "MethodInfo{" + "name='" + name + '\'' + ", paramTypes=" + paramTypes + '}'; 659 } 660 } 661 normalize(Type type)662 private static String normalize(Type type) { 663 return type.getClassName().replace('$', '.'); 664 } 665 666 static class MethodExtraInfo { 667 private final boolean isStatic; 668 private final String returnType; 669 670 /** Create a MethodExtraInfo from ASM in-memory representation (an Android framework method). */ MethodExtraInfo(MethodNode method)671 public MethodExtraInfo(MethodNode method) { 672 this.isStatic = (method.access & Opcodes.ACC_STATIC) != 0; 673 this.returnType = typeWithoutGenerics(normalize(Type.getReturnType(method.desc))); 674 } 675 676 /** Create a MethodExtraInfo from AST (an @Implementation method in a shadow class). */ MethodExtraInfo(ExecutableElement methodElement)677 public MethodExtraInfo(ExecutableElement methodElement) { 678 this.isStatic = methodElement.getModifiers().contains(Modifier.STATIC); 679 680 TypeMirror rtType = methodElement.getReturnType(); 681 String rt = canonicalize(rtType); 682 // If return type is annotated with @ClassName, then use the indicated type instead. 683 List<? extends AnnotationMirror> annotationMirrors = rtType.getAnnotationMirrors(); 684 for (AnnotationMirror am : annotationMirrors) { 685 if (am.getAnnotationType().toString().equals(ClassName.class.getName())) { 686 Map<? extends ExecutableElement, ? extends AnnotationValue> annotationEntries = 687 am.getElementValues(); 688 Set<? extends ExecutableElement> keys = annotationEntries.keySet(); 689 for (ExecutableElement key : keys) { 690 if ("value()".equals(key.toString())) { 691 AnnotationValue annotationValue = annotationEntries.get(key); 692 rt = annotationValue.getValue().toString().replace('$', '.'); 693 break; 694 } 695 } 696 break; 697 } 698 } 699 this.returnType = typeWithoutGenerics(rt); 700 } 701 702 @Override equals(Object o)703 public boolean equals(Object o) { 704 if (this == o) { 705 return true; 706 } 707 if (!(o instanceof MethodExtraInfo)) { 708 return false; 709 } 710 MethodExtraInfo that = (MethodExtraInfo) o; 711 return isStatic == that.isStatic && Objects.equals(returnType, that.returnType); 712 } 713 714 @Override hashCode()715 public int hashCode() { 716 return Objects.hash(isStatic, returnType); 717 } 718 } 719 } 720