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