• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import org.gradle.api.Plugin
2import org.gradle.api.Project
3import org.objectweb.asm.ClassReader
4import org.objectweb.asm.tree.AnnotationNode
5import org.objectweb.asm.tree.ClassNode
6import org.objectweb.asm.tree.MethodNode
7
8import java.util.jar.JarEntry
9import java.util.jar.JarInputStream
10import java.util.regex.Pattern
11
12import static org.objectweb.asm.Opcodes.ACC_PRIVATE
13import static org.objectweb.asm.Opcodes.ACC_PROTECTED
14import static org.objectweb.asm.Opcodes.ACC_PUBLIC
15import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC
16
17class CheckApiChangesPlugin implements Plugin<Project> {
18    @Override
19    void apply(Project project) {
20        project.extensions.create("checkApiChanges", CheckApiChangesExtension)
21
22        project.configurations {
23            checkApiChangesFrom
24            checkApiChangesTo
25        }
26
27        project.afterEvaluate {
28            project.checkApiChanges.from.each {
29                project.dependencies.checkApiChangesFrom(it) {
30                    transitive = false
31                    force = true
32                }
33            }
34
35            project.checkApiChanges.to.findAll { it instanceof String }.each {
36                project.dependencies.checkApiChangesTo(it) {
37                    transitive = false
38                    force = true
39                }
40            }
41        }
42
43        project.task('checkForApiChanges', dependsOn: 'jar') {
44            doLast {
45                Map<ClassMethod, Change> changedClassMethods = new TreeMap<>()
46
47                def fromUrls = project.configurations.checkApiChangesFrom*.toURI()*.toURL()
48                println "fromUrls = ${fromUrls*.toString()*.replaceAll("^.*/", "")}"
49
50                def jarUrls = project.checkApiChanges.to
51                        .findAll { it instanceof Project }
52                        .collect { it.jar.archivePath.toURL() }
53                def toUrls = jarUrls + project.configurations.checkApiChangesTo*.toURI()*.toURL()
54                println "toUrls = ${toUrls*.toString()*.replaceAll("^.*/", "")}"
55
56                Analysis prev = new Analysis(fromUrls)
57                Analysis cur = new Analysis(toUrls)
58
59                Set<String> allMethods = new TreeSet<>(prev.classMethods.keySet())
60                allMethods.addAll(cur.classMethods.keySet())
61
62                Set<ClassMethod> deprecatedNotRemoved = new TreeSet<>()
63                Set<ClassMethod> newlyDeprecated = new TreeSet<>()
64
65                for (String classMethodName : allMethods) {
66                    ClassMethod prevClassMethod = prev.classMethods.get(classMethodName)
67                    ClassMethod curClassMethod = cur.classMethods.get(classMethodName)
68
69                    if (prevClassMethod == null) {
70                        // added
71                        if (curClassMethod.visible) {
72                            changedClassMethods.put(curClassMethod, Change.ADDED)
73                        }
74                    } else if (curClassMethod == null) {
75                        def theClass = prevClassMethod.classNode.name.replace('/', '.')
76                        def methodDesc = prevClassMethod.methodDesc
77                        while (curClassMethod == null && cur.parents[theClass] != null) {
78                            theClass = cur.parents[theClass]
79                            def parentMethodName = "${theClass}#${methodDesc}"
80                            curClassMethod = cur.classMethods[parentMethodName]
81                        }
82
83                        // removed
84                        if (curClassMethod == null && prevClassMethod.visible && !prevClassMethod.deprecated) {
85                            if (classMethodName.contains("getActivityTitle")) {
86                                println "hi!"
87                            }
88                            changedClassMethods.put(prevClassMethod, Change.REMOVED)
89                        }
90                    } else {
91                        if (prevClassMethod.deprecated) {
92                            deprecatedNotRemoved << prevClassMethod;
93                        } else if (curClassMethod.deprecated) {
94                            newlyDeprecated << prevClassMethod;
95                        }
96//                        println "changed: $classMethodName"
97                    }
98                }
99
100                String prevClassName = null
101                def introClass = { classMethod ->
102                    if (classMethod.className != prevClassName) {
103                        prevClassName = classMethod.className
104                        println "\n$prevClassName:"
105                    }
106                }
107
108                def entryPoints = project.checkApiChanges.entryPoints
109                Closure matchesEntryPoint = { ClassMethod classMethod ->
110                    for (String entryPoint : entryPoints) {
111                        if (classMethod.className.matches(entryPoint)) {
112                            return true
113                        }
114                    }
115                    return false
116                }
117
118                def expectedREs = project.checkApiChanges.expectedChanges.collect { Pattern.compile(it) }
119
120                for (Map.Entry<ClassMethod, Change> change : changedClassMethods.entrySet()) {
121                    def classMethod = change.key
122                    def changeType = change.value
123
124                    def showAllChanges = true // todo: only show stuff that's interesting...
125                    if (matchesEntryPoint(classMethod) || showAllChanges) {
126                        String classMethodDesc = classMethod.desc
127                        def expected = expectedREs.any { it.matcher(classMethodDesc).find() }
128                        if (!expected) {
129                            introClass(classMethod)
130
131                            switch (changeType) {
132                                case Change.ADDED:
133                                    println "+ ${classMethod.methodDesc}"
134                                    break
135                                case Change.REMOVED:
136                                    println "- ${classMethod.methodDesc}"
137                                    break
138                            }
139                        }
140                    }
141                }
142
143                if (!deprecatedNotRemoved.empty) {
144                    println "\nDeprecated but not removed:"
145                    for (ClassMethod classMethod : deprecatedNotRemoved) {
146                        introClass(classMethod)
147                        println "* ${classMethod.methodDesc}"
148                    }
149                }
150
151                if (!newlyDeprecated.empty) {
152                    println "\nNewly deprecated:"
153                    for (ClassMethod classMethod : newlyDeprecated) {
154                        introClass(classMethod)
155                        println "* ${classMethod.methodDesc}"
156                    }
157                }
158            }
159        }
160    }
161
162    static class Analysis {
163        final Map<String, String> parents = new HashMap<>()
164        final Map<String, ClassMethod> classMethods = new HashMap<>()
165
166        Analysis(List<URL> baseUrls) {
167            for (URL url : baseUrls) {
168                if (url.protocol == 'file') {
169                    def file = new File(url.path)
170                    def stream = new FileInputStream(file)
171                    def jarStream = new JarInputStream(stream)
172                    while (true) {
173                        JarEntry entry = jarStream.nextJarEntry
174                        if (entry == null) break
175
176                        if (!entry.directory && entry.name.endsWith(".class")) {
177                            def reader = new ClassReader(jarStream)
178                            def classNode = new ClassNode()
179                            reader.accept(classNode, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES)
180
181                            def superName = classNode.superName.replace('/', '.')
182                            if (!"java.lang.Object".equals(superName)) {
183                                parents[classNode.name.replace('/', '.')] = superName
184                            }
185
186                            if (bitSet(classNode.access, ACC_PUBLIC) || bitSet(classNode.access, ACC_PROTECTED)) {
187                                for (MethodNode method : classNode.methods) {
188                                    def classMethod = new ClassMethod(classNode, method, url)
189                                    if (!bitSet(method.access, ACC_SYNTHETIC)) {
190                                        classMethods.put(classMethod.desc, classMethod)
191                                    }
192                                }
193                            }
194                        }
195                    }
196                    stream.close()
197                }
198            }
199            classMethods
200        }
201
202    }
203
204    static enum Change {
205        REMOVED,
206        ADDED,
207    }
208
209    static class ClassMethod implements Comparable<ClassMethod> {
210        final ClassNode classNode
211        final MethodNode methodNode
212        final URL originUrl
213
214        ClassMethod(ClassNode classNode, MethodNode methodNode, URL originUrl) {
215            this.classNode = classNode
216            this.methodNode = methodNode
217            this.originUrl = originUrl
218        }
219
220        boolean equals(o) {
221            if (this.is(o)) return true
222            if (getClass() != o.class) return false
223
224            ClassMethod that = (ClassMethod) o
225
226            if (classNode.name != that.classNode.name) return false
227            if (methodNode.name != that.methodNode.name) return false
228            if (methodNode.signature != that.methodNode.signature) return false
229
230            return true
231        }
232
233        int hashCode() {
234            int result
235            result = (classNode.name != null ? classNode.name.hashCode() : 0)
236            result = 31 * result + (methodNode.name != null ? methodNode.name.hashCode() : 0)
237            result = 31 * result + (methodNode.signature != null ? methodNode.signature.hashCode() : 0)
238            return result
239        }
240
241        public String getDesc() {
242            return "$className#$methodDesc"
243        }
244
245        boolean hasParent() {
246            parentClassName() != "java/lang/Object"
247        }
248
249        String parentClassName() {
250            classNode.superName
251        }
252
253        private String getMethodDesc() {
254            def args = new StringBuilder()
255            def returnType = new StringBuilder()
256            def buf = args
257
258            int arrayDepth = 0
259            def write = { typeName ->
260                if (buf.size() > 0) buf.append(", ")
261                buf.append(typeName)
262                for (; arrayDepth > 0; arrayDepth--) {
263                    buf.append("[]")
264                }
265            }
266
267            def chars = methodNode.desc.toCharArray()
268            def i = 0
269
270            def readObj = {
271                if (buf.size() > 0) buf.append(", ")
272                def objNameBuf = new StringBuilder()
273                for (; i < chars.length; i++) {
274                    char c = chars[i]
275                    if (c == ';' as char) break
276                    objNameBuf.append((c == '/' as char) ? '.' : c)
277                }
278                buf.append(objNameBuf.toString().replaceAll(/^java\.lang\./, ''))
279            }
280
281            for (; i < chars.length;) {
282                def c = chars[i++]
283                switch (c) {
284                    case '(': break;
285                    case ')': buf = returnType; break;
286                    case '[': arrayDepth++; break;
287                    case 'Z': write('boolean'); break;
288                    case 'B': write('byte'); break;
289                    case 'S': write('short'); break;
290                    case 'I': write('int'); break;
291                    case 'J': write('long'); break;
292                    case 'F': write('float'); break;
293                    case 'D': write('double'); break;
294                    case 'C': write('char'); break;
295                    case 'L': readObj(); break;
296                    case 'V': write('void'); break;
297                }
298            }
299            "$methodAccessString ${isHiddenApi() ? "@HiddenApi " : ""}${isImplementation() ? "@Implementation " : ""}$methodNode.name(${args.toString()}): ${returnType.toString()}"
300        }
301
302        @Override
303        public String toString() {
304            internalName
305        }
306
307        private String getInternalName() {
308            classNode.name + "#$methodInternalName"
309        }
310
311        private String getMethodInternalName() {
312            "$methodNode.name$methodNode.desc"
313        }
314
315        private String getSignature() {
316            methodNode.signature == null ? "()V" : methodNode.signature
317        }
318
319        private String getClassName() {
320            classNode.name.replace('/', '.')
321        }
322
323        boolean isDeprecated() {
324            containsAnnotation(classNode.visibleAnnotations, "Ljava/lang/Deprecated;") ||
325                    containsAnnotation(methodNode.visibleAnnotations, "Ljava/lang/Deprecated;")
326        }
327
328        boolean isImplementation() {
329            containsAnnotation(methodNode.visibleAnnotations, "Lorg/robolectric/annotation/Implementation;")
330        }
331
332        boolean isHiddenApi() {
333            containsAnnotation(methodNode.visibleAnnotations, "Lorg/robolectric/annotation/HiddenApi;")
334        }
335
336        String getMethodAccessString() {
337            return getAccessString(methodNode.access)
338        }
339
340        private String getClassAccessString() {
341            return getAccessString(classNode.access)
342        }
343
344        String getAccessString(int access) {
345            if (bitSet(access, ACC_PROTECTED)) {
346                return "protected"
347            } else if (bitSet(access, ACC_PUBLIC)) {
348                return "public"
349            } else if (bitSet(access, ACC_PRIVATE)) {
350                return "private"
351            } else {
352                return "[package]"
353            }
354        }
355
356        boolean isVisible() {
357            (bitSet(classNode.access, ACC_PUBLIC) || bitSet(classNode.access, ACC_PROTECTED)) &&
358                    (bitSet(methodNode.access, ACC_PUBLIC) || bitSet(methodNode.access, ACC_PROTECTED)) &&
359                    !bitSet(classNode.access, ACC_SYNTHETIC) &&
360                    !(classNode.name =~ /\$[0-9]/) &&
361                    !(methodNode.name =~ /^access\$/ || methodNode.name == '<clinit>')
362        }
363
364        private static boolean containsAnnotation(List<AnnotationNode> annotations, String annotationInternalName) {
365            for (AnnotationNode annotationNode : annotations) {
366                if (annotationNode.desc == annotationInternalName) {
367                    return true
368                }
369            }
370            return false
371        }
372
373        @Override
374        int compareTo(ClassMethod o) {
375            internalName <=> o.internalName
376        }
377    }
378
379    private static boolean bitSet(int field, int bit) {
380        (field & bit) == bit
381    }
382}
383
384class CheckApiChangesExtension {
385    String[] from
386    Object[] to
387
388    String[] entryPoints
389    String[] expectedChanges
390}