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