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