• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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