Home | History | Annotate | Download | only in groovy
      1 import org.gradle.api.Plugin
      2 import org.gradle.api.Project
      3 import org.objectweb.asm.ClassReader
      4 import org.objectweb.asm.tree.AnnotationNode
      5 import org.objectweb.asm.tree.ClassNode
      6 import org.objectweb.asm.tree.MethodNode
      7 
      8 import java.util.jar.JarEntry
      9 import java.util.jar.JarInputStream
     10 import java.util.regex.Pattern
     11 
     12 import static org.objectweb.asm.Opcodes.ACC_PROTECTED
     13 import static org.objectweb.asm.Opcodes.ACC_PUBLIC
     14 import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC
     15 
     16 class 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 
    355 class CheckApiChangesExtension {
    356     String[] from
    357     Object[] to
    358 
    359     String[] entryPoints
    360     String[] expectedChanges
    361 }