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 }