• 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 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 
sdksMatching(Implementation implementation, int classMinSdk, int classMaxSdk)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 
loadSdksOnce()74   private synchronized void loadSdksOnce() {
75     if (!loaded) {
76       sdks.addAll(loadFromSdksFile("/sdks.txt"));
77       loaded = true;
78     }
79   }
80 
loadFromSdksFile(String resourceFileName)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 
canonicalize(TypeMirror typeMirror)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 
typeWithoutGenerics(String paramType)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 
Sdk(String path)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      */
verifyMethod(TypeElement sdkClassElem, ExecutableElement methodElement, boolean looseSignatures)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 
suppressWarnings(ExecutableElement methodElement, String warningName)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 
typeIsNumeric(MethodExtraInfo sdkMethod, MethodExtraInfo implMethod)192     private boolean typeIsNumeric(MethodExtraInfo sdkMethod, MethodExtraInfo implMethod) {
193       return implMethod.returnType.equals("java.lang.Number")
194       && isNumericType(sdkMethod.returnType);
195     }
196 
typeIsOkForLooseSignatures(MethodExtraInfo implMethod, MethodExtraInfo sdkMethod)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 
isNumericType(String type)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      */
getClassInfo(String name)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      */
readSdkInt()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 
ensureJar()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 
copyResourceToFile(String resourcePath)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 
loadClassNode(String name)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
compareTo(Sdk sdk)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 
ClassInfo()316     private ClassInfo() {
317     }
318 
ClassInfo(ClassNode classNode)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 
findMethod(ExecutableElement methodElement, boolean looseSignatures)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). */
MethodInfo(MethodNode 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). */
MethodInfo(String name, int size)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). */
MethodInfo(ExecutableElement methodElement)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 
cleanMethodName(ExecutableElement methodElement)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 
erase()383     public MethodInfo erase() {
384       return new MethodInfo(name, paramTypes.size());
385     }
386 
387     @Override
equals(Object o)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
hashCode()401     public int hashCode() {
402       return Objects.hash(name, paramTypes);
403     }
404     @Override
toString()405     public String toString() {
406       return "MethodInfo{"
407           + "name='" + name + '\''
408           + ", paramTypes=" + paramTypes
409           + '}';
410     }
411   }
412 
normalize(Type type)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 
MethodExtraInfo(MethodNode method)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 
MethodExtraInfo(ExecutableElement methodElement)426     public MethodExtraInfo(ExecutableElement methodElement) {
427       this.isStatic = methodElement.getModifiers().contains(Modifier.STATIC);
428       this.returnType = typeWithoutGenerics(canonicalize(methodElement.getReturnType()));
429     }
430 
431     @Override
equals(Object o)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
hashCode()445     public int hashCode() {
446       return Objects.hash(isStatic, returnType);
447     }
448   }
449 }
450