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}