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