blob: c0671c598e630789b885f9b902505480fd56c11d [file] [log] [blame]
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
import java.util.jar.JarEntry
import java.util.jar.JarInputStream
import java.util.regex.Pattern
import static org.objectweb.asm.Opcodes.ACC_PRIVATE
import static org.objectweb.asm.Opcodes.ACC_PROTECTED
import static org.objectweb.asm.Opcodes.ACC_PUBLIC
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC
class CheckApiChangesPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.create("checkApiChanges", CheckApiChangesExtension)
project.configurations {
checkApiChangesFrom
checkApiChangesTo
}
project.afterEvaluate {
project.checkApiChanges.from.each {
project.dependencies.checkApiChangesFrom(it) {
transitive = false
force = true
}
}
project.checkApiChanges.to.findAll { it instanceof String }.each {
project.dependencies.checkApiChangesTo(it) {
transitive = false
force = true
}
}
}
project.task('checkForApiChanges', dependsOn: 'jar') {
doLast {
Map<ClassMethod, Change> changedClassMethods = new TreeMap<>()
def fromUrls = project.configurations.checkApiChangesFrom*.toURI()*.toURL()
println "fromUrls = ${fromUrls*.toString()*.replaceAll("^.*/", "")}"
def jarUrls = project.checkApiChanges.to
.findAll { it instanceof Project }
.collect { it.jar.archivePath.toURL() }
def toUrls = jarUrls + project.configurations.checkApiChangesTo*.toURI()*.toURL()
println "toUrls = ${toUrls*.toString()*.replaceAll("^.*/", "")}"
Analysis prev = new Analysis(fromUrls)
Analysis cur = new Analysis(toUrls)
Set<String> allMethods = new TreeSet<>(prev.classMethods.keySet())
allMethods.addAll(cur.classMethods.keySet())
Set<ClassMethod> deprecatedNotRemoved = new TreeSet<>()
Set<ClassMethod> newlyDeprecated = new TreeSet<>()
for (String classMethodName : allMethods) {
ClassMethod prevClassMethod = prev.classMethods.get(classMethodName)
ClassMethod curClassMethod = cur.classMethods.get(classMethodName)
if (prevClassMethod == null) {
// added
if (curClassMethod.visible) {
changedClassMethods.put(curClassMethod, Change.ADDED)
}
} else if (curClassMethod == null) {
def theClass = prevClassMethod.classNode.name.replace('/', '.')
def methodDesc = prevClassMethod.methodDesc
while (curClassMethod == null && cur.parents[theClass] != null) {
theClass = cur.parents[theClass]
def parentMethodName = "${theClass}#${methodDesc}"
curClassMethod = cur.classMethods[parentMethodName]
}
// removed
if (curClassMethod == null && prevClassMethod.visible && !prevClassMethod.deprecated) {
if (classMethodName.contains("getActivityTitle")) {
println "hi!"
}
changedClassMethods.put(prevClassMethod, Change.REMOVED)
}
} else {
if (prevClassMethod.deprecated) {
deprecatedNotRemoved << prevClassMethod;
} else if (curClassMethod.deprecated) {
newlyDeprecated << prevClassMethod;
}
// println "changed: $classMethodName"
}
}
String prevClassName = null
def introClass = { classMethod ->
if (classMethod.className != prevClassName) {
prevClassName = classMethod.className
println "\n$prevClassName:"
}
}
def entryPoints = project.checkApiChanges.entryPoints
Closure matchesEntryPoint = { ClassMethod classMethod ->
for (String entryPoint : entryPoints) {
if (classMethod.className.matches(entryPoint)) {
return true
}
}
return false
}
def expectedREs = project.checkApiChanges.expectedChanges.collect { Pattern.compile(it) }
for (Map.Entry<ClassMethod, Change> change : changedClassMethods.entrySet()) {
def classMethod = change.key
def changeType = change.value
def showAllChanges = true // todo: only show stuff that's interesting...
if (matchesEntryPoint(classMethod) || showAllChanges) {
String classMethodDesc = classMethod.desc
def expected = expectedREs.any { it.matcher(classMethodDesc).find() }
if (!expected) {
introClass(classMethod)
switch (changeType) {
case Change.ADDED:
println "+ ${classMethod.methodDesc}"
break
case Change.REMOVED:
println "- ${classMethod.methodDesc}"
break
}
}
}
}
if (!deprecatedNotRemoved.empty) {
println "\nDeprecated but not removed:"
for (ClassMethod classMethod : deprecatedNotRemoved) {
introClass(classMethod)
println "* ${classMethod.methodDesc}"
}
}
if (!newlyDeprecated.empty) {
println "\nNewly deprecated:"
for (ClassMethod classMethod : newlyDeprecated) {
introClass(classMethod)
println "* ${classMethod.methodDesc}"
}
}
}
}
}
static class Analysis {
final Map<String, String> parents = new HashMap<>()
final Map<String, ClassMethod> classMethods = new HashMap<>()
Analysis(List<URL> baseUrls) {
for (URL url : baseUrls) {
if (url.protocol == 'file') {
def file = new File(url.path)
def stream = new FileInputStream(file)
def jarStream = new JarInputStream(stream)
while (true) {
JarEntry entry = jarStream.nextJarEntry
if (entry == null) break
if (!entry.directory && entry.name.endsWith(".class")) {
def reader = new ClassReader(jarStream)
def classNode = new ClassNode()
reader.accept(classNode, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES)
def superName = classNode.superName.replace('/', '.')
if (!"java.lang.Object".equals(superName)) {
parents[classNode.name.replace('/', '.')] = superName
}
if (bitSet(classNode.access, ACC_PUBLIC) || bitSet(classNode.access, ACC_PROTECTED)) {
for (MethodNode method : classNode.methods) {
def classMethod = new ClassMethod(classNode, method, url)
if (!bitSet(method.access, ACC_SYNTHETIC)) {
classMethods.put(classMethod.desc, classMethod)
}
}
}
}
}
stream.close()
}
}
classMethods
}
}
static enum Change {
REMOVED,
ADDED,
}
static class ClassMethod implements Comparable<ClassMethod> {
final ClassNode classNode
final MethodNode methodNode
final URL originUrl
ClassMethod(ClassNode classNode, MethodNode methodNode, URL originUrl) {
this.classNode = classNode
this.methodNode = methodNode
this.originUrl = originUrl
}
boolean equals(o) {
if (this.is(o)) return true
if (getClass() != o.class) return false
ClassMethod that = (ClassMethod) o
if (classNode.name != that.classNode.name) return false
if (methodNode.name != that.methodNode.name) return false
if (methodNode.signature != that.methodNode.signature) return false
return true
}
int hashCode() {
int result
result = (classNode.name != null ? classNode.name.hashCode() : 0)
result = 31 * result + (methodNode.name != null ? methodNode.name.hashCode() : 0)
result = 31 * result + (methodNode.signature != null ? methodNode.signature.hashCode() : 0)
return result
}
public String getDesc() {
return "$className#$methodDesc"
}
boolean hasParent() {
parentClassName() != "java/lang/Object"
}
String parentClassName() {
classNode.superName
}
private String getMethodDesc() {
def args = new StringBuilder()
def returnType = new StringBuilder()
def buf = args
int arrayDepth = 0
def write = { typeName ->
if (buf.size() > 0) buf.append(", ")
buf.append(typeName)
for (; arrayDepth > 0; arrayDepth--) {
buf.append("[]")
}
}
def chars = methodNode.desc.toCharArray()
def i = 0
def readObj = {
if (buf.size() > 0) buf.append(", ")
def objNameBuf = new StringBuilder()
for (; i < chars.length; i++) {
char c = chars[i]
if (c == ';' as char) break
objNameBuf.append((c == '/' as char) ? '.' : c)
}
buf.append(objNameBuf.toString().replaceAll(/^java\.lang\./, ''))
}
for (; i < chars.length;) {
def c = chars[i++]
switch (c) {
case '(': break;
case ')': buf = returnType; break;
case '[': arrayDepth++; break;
case 'Z': write('boolean'); break;
case 'B': write('byte'); break;
case 'S': write('short'); break;
case 'I': write('int'); break;
case 'J': write('long'); break;
case 'F': write('float'); break;
case 'D': write('double'); break;
case 'C': write('char'); break;
case 'L': readObj(); break;
case 'V': write('void'); break;
}
}
"$methodAccessString ${isHiddenApi() ? "@HiddenApi " : ""}${isImplementation() ? "@Implementation " : ""}$methodNode.name(${args.toString()}): ${returnType.toString()}"
}
@Override
public String toString() {
internalName
}
private String getInternalName() {
classNode.name + "#$methodInternalName"
}
private String getMethodInternalName() {
"$methodNode.name$methodNode.desc"
}
private String getSignature() {
methodNode.signature == null ? "()V" : methodNode.signature
}
private String getClassName() {
classNode.name.replace('/', '.')
}
boolean isDeprecated() {
containsAnnotation(classNode.visibleAnnotations, "Ljava/lang/Deprecated;") ||
containsAnnotation(methodNode.visibleAnnotations, "Ljava/lang/Deprecated;")
}
boolean isImplementation() {
containsAnnotation(methodNode.visibleAnnotations, "Lorg/robolectric/annotation/Implementation;")
}
boolean isHiddenApi() {
containsAnnotation(methodNode.visibleAnnotations, "Lorg/robolectric/annotation/HiddenApi;")
}
String getMethodAccessString() {
return getAccessString(methodNode.access)
}
private String getClassAccessString() {
return getAccessString(classNode.access)
}
String getAccessString(int access) {
if (bitSet(access, ACC_PROTECTED)) {
return "protected"
} else if (bitSet(access, ACC_PUBLIC)) {
return "public"
} else if (bitSet(access, ACC_PRIVATE)) {
return "private"
} else {
return "[package]"
}
}
boolean isVisible() {
(bitSet(classNode.access, ACC_PUBLIC) || bitSet(classNode.access, ACC_PROTECTED)) &&
(bitSet(methodNode.access, ACC_PUBLIC) || bitSet(methodNode.access, ACC_PROTECTED)) &&
!bitSet(classNode.access, ACC_SYNTHETIC) &&
!(classNode.name =~ /\$[0-9]/) &&
!(methodNode.name =~ /^access\$/ || methodNode.name == '<clinit>')
}
private static boolean containsAnnotation(List<AnnotationNode> annotations, String annotationInternalName) {
for (AnnotationNode annotationNode : annotations) {
if (annotationNode.desc == annotationInternalName) {
return true
}
}
return false
}
@Override
int compareTo(ClassMethod o) {
internalName <=> o.internalName
}
}
private static boolean bitSet(int field, int bit) {
(field & bit) == bit
}
}
class CheckApiChangesExtension {
String[] from
Object[] to
String[] entryPoints
String[] expectedChanges
}