blob: f9be83677161e9b035536e12fb771d62c31f56e5 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.metalava
import com.android.tools.metalava.model.AnnotationItem
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.ConstructorItem
import com.android.tools.metalava.model.FieldItem
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.MergedCodebase
import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.PackageItem
import com.android.tools.metalava.model.ParameterItem
import com.android.tools.metalava.model.PropertyItem
import com.android.tools.metalava.model.VisitCandidate
import com.android.tools.metalava.model.visitors.ApiVisitor
import com.intellij.util.containers.Stack
import java.util.function.Predicate
/**
* Visitor which visits all items in two matching codebases and
* matches up the items and invokes [compare] on each pair, or
* [added] or [removed] when items are not matched
*/
open class ComparisonVisitor(
/**
* Whether constructors should be visited as part of a [#visitMethod] call
* instead of just a [#visitConstructor] call. Helps simplify visitors that
* don't care to distinguish between the two cases. Defaults to true.
*/
val visitConstructorsAsMethods: Boolean = true,
/**
* Normally if a new item is found, the visitor will
* only visit the top level newly added item, not all
* of its children. This flags enables you to request
* all individual items to also be visited.
*/
val visitAddedItemsRecursively: Boolean = false
) {
open fun compare(old: Item, new: Item) {}
open fun added(new: Item) {}
open fun removed(old: Item, from: Item?) {}
open fun compare(old: PackageItem, new: PackageItem) {}
open fun compare(old: ClassItem, new: ClassItem) {}
open fun compare(old: ConstructorItem, new: ConstructorItem) {}
open fun compare(old: MethodItem, new: MethodItem) {}
open fun compare(old: FieldItem, new: FieldItem) {}
open fun compare(old: PropertyItem, new: PropertyItem) {}
open fun compare(old: ParameterItem, new: ParameterItem) {}
open fun added(new: PackageItem) {}
open fun added(new: ClassItem) {}
open fun added(new: ConstructorItem) {}
open fun added(new: MethodItem) {}
open fun added(new: FieldItem) {}
open fun added(new: PropertyItem) {}
open fun added(new: ParameterItem) {}
open fun removed(old: PackageItem, from: Item?) {}
open fun removed(old: ClassItem, from: Item?) {}
open fun removed(old: ConstructorItem, from: ClassItem?) {}
open fun removed(old: MethodItem, from: ClassItem?) {}
open fun removed(old: FieldItem, from: ClassItem?) {}
open fun removed(old: PropertyItem, from: ClassItem?) {}
open fun removed(old: ParameterItem, from: MethodItem?) {}
}
class CodebaseComparator {
/**
* Visits this codebase and compares it with another codebase, informing the visitors about
* the correlations and differences that it finds
*/
fun compare(visitor: ComparisonVisitor, old: Codebase, new: Codebase, filter: Predicate<Item>? = null) {
// Algorithm: build up two trees (by nesting level); then visit the
// two trees
val oldTree = createTree(old, filter)
val newTree = createTree(new, filter)
/* Debugging:
println("Old:\n${ItemTree.prettyPrint(oldTree)}")
println("New:\n${ItemTree.prettyPrint(newTree)}")
*/
compare(visitor, oldTree, newTree, null, null, filter)
}
fun compare(visitor: ComparisonVisitor, old: MergedCodebase, new: MergedCodebase, filter: Predicate<Item>? = null) {
// Algorithm: build up two trees (by nesting level); then visit the
// two trees
val oldTree = createTree(old, filter)
val newTree = createTree(new, filter)
/* Debugging:
println("Old:\n${ItemTree.prettyPrint(oldTree)}")
println("New:\n${ItemTree.prettyPrint(newTree)}")
*/
compare(visitor, oldTree, newTree, null, null, filter)
}
private fun compare(
visitor: ComparisonVisitor,
oldList: List<ItemTree>,
newList: List<ItemTree>,
newParent: Item?,
oldParent: Item?,
filter: Predicate<Item>?
) {
// Debugging tip: You can print out a tree like this: ItemTree.prettyPrint(list)
var index1 = 0
var index2 = 0
val length1 = oldList.size
val length2 = newList.size
while (true) {
if (index1 < length1) {
if (index2 < length2) {
// Compare the items
val oldTree = oldList[index1]
val newTree = newList[index2]
val old = oldTree.item()
val new = newTree.item()
val compare = compare(old, new)
when {
compare > 0 -> {
index2++
if (new.emit) {
visitAdded(new, oldParent, visitor, newTree, filter)
}
}
compare < 0 -> {
index1++
if (old.emit) {
visitRemoved(old, oldTree, visitor, newParent, filter)
}
}
else -> {
if (new.emit) {
if (old.emit) {
visitCompare(visitor, old, new)
} else {
visitAdded(new, oldParent, visitor, newTree, filter)
}
} else {
if (old.emit) {
visitRemoved(old, oldTree, visitor, newParent, filter)
}
}
// Compare the children (recurse)
compare(visitor, oldTree.children, newTree.children, newTree.item(), oldTree.item(), filter)
index1++
index2++
}
}
} else {
// All the remaining items in oldList have been deleted
while (index1 < length1) {
val oldTree = oldList[index1++]
val old = oldTree.item()
visitRemoved(old, oldTree, visitor, newParent, filter)
}
}
} else if (index2 < length2) {
// All the remaining items in newList have been added
while (index2 < length2) {
val newTree = newList[index2++]
val new = newTree.item()
visitAdded(new, oldParent, visitor, newTree, filter)
}
} else {
break
}
}
}
private fun visitAdded(
new: Item,
oldParent: Item?,
visitor: ComparisonVisitor,
newTree: ItemTree,
filter: Predicate<Item>?
) {
// If it's a method, we may not have added a new method,
// we may simply have inherited it previously and overriding
// it now (or in the case of signature files, identical overrides
// are not explicitly listed and therefore not added to the model)
val inherited =
if (new is MethodItem && oldParent is ClassItem) {
oldParent.findMethod(
template = new,
includeSuperClasses = true,
includeInterfaces = true
)?.duplicate(oldParent)
} else {
null
}
if (inherited != null) {
visitCompare(visitor, inherited, new)
// Compare the children (recurse)
if (inherited.parameters().isNotEmpty()) {
val parameters = inherited.parameters().map { ItemTree(it) }.toList()
compare(visitor, parameters, newTree.children, newTree.item(), inherited, filter)
}
} else {
visitAdded(visitor, new)
}
}
private fun visitAdded(visitor: ComparisonVisitor, new: Item) {
if (visitor.visitAddedItemsRecursively) {
new.accept(object : ApiVisitor() {
override fun visitItem(item: Item) {
doVisitAdded(visitor, item)
}
})
} else {
doVisitAdded(visitor, new)
}
}
@Suppress("USELESS_CAST") // Overloaded visitor methods: be explicit about which one is being invoked
private fun doVisitAdded(visitor: ComparisonVisitor, item: Item) {
visitor.added(item)
when (item) {
is PackageItem -> visitor.added(item)
is ClassItem -> visitor.added(item)
is MethodItem -> {
if (visitor.visitConstructorsAsMethods) {
visitor.added(item)
} else {
if (item is ConstructorItem) {
visitor.added(item as ConstructorItem)
} else {
visitor.added(item as MethodItem)
}
}
}
is FieldItem -> visitor.added(item)
is ParameterItem -> visitor.added(item)
is PropertyItem -> visitor.added(item)
}
}
private fun visitRemoved(
old: Item,
oldTree: ItemTree,
visitor: ComparisonVisitor,
newParent: Item?,
filter: Predicate<Item>?
) {
// If it's a method, we may not have removed the method, we may have simply
// removed an override and are now inheriting the method from a superclass.
// Alternatively, it may have always truly been an inherited method, but if the base
// class was hidden then the signature file may have listed the method as being
// declared on the subclass
val inheritedMethod =
if (old is MethodItem && !old.isConstructor() && newParent is ClassItem) {
val superMethod = newParent.findPredicateMethodWithSuper(old, filter)
if (superMethod != null && (filter == null || filter.test(superMethod))) {
superMethod.duplicate(newParent)
} else {
null
}
} else {
null
}
if (inheritedMethod != null) {
visitCompare(visitor, old, inheritedMethod)
// Compare the children (recurse)
if (inheritedMethod.parameters().isNotEmpty()) {
val parameters = inheritedMethod.parameters().map { ItemTree(it) }.toList()
compare(visitor, oldTree.children, parameters, oldTree.item(), inheritedMethod, filter)
}
return
}
// fields may also be moved to superclasses like methods may
val inheritedField =
if (old is FieldItem && newParent is ClassItem) {
val superField = newParent.findField(
fieldName = old.name(),
includeSuperClasses = true,
includeInterfaces = true
)
if (superField != null && (filter == null || filter.test(superField))) {
superField.duplicate(newParent)
} else {
null
}
} else {
null
}
if (inheritedField != null) {
visitCompare(visitor, old, inheritedField)
return
}
visitRemoved(visitor, old, newParent)
}
@Suppress("USELESS_CAST") // Overloaded visitor methods: be explicit about which one is being invoked
private fun visitRemoved(visitor: ComparisonVisitor, item: Item, from: Item?) {
visitor.removed(item, from)
when (item) {
is PackageItem -> visitor.removed(item, from)
is ClassItem -> visitor.removed(item, from)
is MethodItem -> {
if (visitor.visitConstructorsAsMethods) {
visitor.removed(item, from as ClassItem?)
} else {
if (item is ConstructorItem) {
visitor.removed(item as ConstructorItem, from as ClassItem?)
} else {
visitor.removed(item as MethodItem, from as ClassItem?)
}
}
}
is FieldItem -> visitor.removed(item, from as ClassItem?)
is ParameterItem -> visitor.removed(item, from as MethodItem?)
is PropertyItem -> visitor.removed(item, from as ClassItem?)
}
}
@Suppress("USELESS_CAST") // Overloaded visitor methods: be explicit about which one is being invoked
private fun visitCompare(visitor: ComparisonVisitor, old: Item, new: Item) {
visitor.compare(old, new)
when (old) {
is PackageItem -> visitor.compare(old, new as PackageItem)
is ClassItem -> visitor.compare(old, new as ClassItem)
is MethodItem -> {
if (visitor.visitConstructorsAsMethods) {
visitor.compare(old, new as MethodItem)
} else {
if (old is ConstructorItem) {
visitor.compare(old as ConstructorItem, new as MethodItem)
} else {
visitor.compare(old as MethodItem, new as MethodItem)
}
}
}
is FieldItem -> visitor.compare(old, new as FieldItem)
is ParameterItem -> visitor.compare(old, new as ParameterItem)
is PropertyItem -> visitor.compare(old, new as PropertyItem)
}
}
private fun compare(item1: Item, item2: Item): Int = comparator.compare(item1, item2)
companion object {
/** Sorting rank for types */
private fun typeRank(item: Item): Int {
return when (item) {
is PackageItem -> 0
is MethodItem -> if (item.isConstructor()) 1 else 2
is FieldItem -> 3
is ClassItem -> 4
is ParameterItem -> 5
is AnnotationItem -> 6
is PropertyItem -> 7
else -> 8
}
}
val comparator: Comparator<Item> = Comparator { item1, item2 ->
val typeSort = typeRank(item1) - typeRank(item2)
when {
typeSort != 0 -> typeSort
item1 == item2 -> 0
else -> when (item1) {
is PackageItem -> {
item1.qualifiedName().compareTo((item2 as PackageItem).qualifiedName())
}
is ClassItem -> {
item1.qualifiedName().compareTo((item2 as ClassItem).qualifiedName())
}
is MethodItem -> {
var delta = item1.name().compareTo((item2 as MethodItem).name())
if (delta == 0) {
val parameters1 = item1.parameters()
val parameters2 = item2.parameters()
val parameterCount1 = parameters1.size
val parameterCount2 = parameters2.size
delta = parameterCount1 - parameterCount2
if (delta == 0) {
for (i in 0 until parameterCount1) {
val parameter1 = parameters1[i]
val parameter2 = parameters2[i]
val type1 = parameter1.type().toTypeString(context = parameter1)
val type2 = parameter2.type().toTypeString(context = parameter2)
delta = type1.compareTo(type2)
if (delta != 0) {
// Try a little harder:
// (1) treat varargs and arrays the same, and
// (2) drop java.lang. prefixes from comparisons in wildcard
// signatures since older signature files may have removed
// those
val simpleType1 = parameter1.type().toCanonicalType(parameter1)
val simpleType2 = parameter2.type().toCanonicalType(parameter2)
delta = simpleType1.compareTo(simpleType2)
if (delta != 0) {
// Special case: Kotlin coroutines
if (simpleType1.startsWith("kotlin.coroutines.") && simpleType2.startsWith("kotlin.coroutines.")) {
val t1 = simpleType1.removePrefix("kotlin.coroutines.").removePrefix("experimental.")
val t2 = simpleType2.removePrefix("kotlin.coroutines.").removePrefix("experimental.")
delta = t1.compareTo(t2)
if (delta != 0) {
break
}
} else {
break
}
}
}
}
}
}
delta
}
is FieldItem -> {
item1.name().compareTo((item2 as FieldItem).name())
}
is ParameterItem -> {
item1.parameterIndex.compareTo((item2 as ParameterItem).parameterIndex)
}
is AnnotationItem -> {
(item1.qualifiedName ?: "").compareTo((item2 as AnnotationItem).qualifiedName ?: "")
}
is PropertyItem -> {
item1.name().compareTo((item2 as PropertyItem).name())
}
else -> {
error("Unexpected item type ${item1.javaClass}")
}
}
}
}
val treeComparator: Comparator<ItemTree> = Comparator { item1, item2 ->
comparator.compare(item1.item, item2.item())
}
}
private fun ensureSorted(item: ItemTree) {
item.children.sortWith(treeComparator)
for (child in item.children) {
ensureSorted(child)
}
}
/**
* Sorts and removes duplicate items.
* The kept item will be an unhidden item if possible.
* Ties are broken in favor of keeping children having lower indices
*/
private fun removeDuplicates(item: ItemTree) {
item.children.sortWith(treeComparator)
val children = item.children
var i = children.count() - 2
while (i >= 0) {
val child = children[i]
val prev = children[i + 1]
if (comparator.compare(child.item, prev.item) == 0) {
if (prev.item!!.emit && !child.item!!.emit) {
// merge child into prev because prev is emitted
prev.children += child.children
children.removeAt(i)
} else {
// merge prev into child because child was specified first
child.children += prev.children
children.removeAt(i + 1)
}
}
i--
}
for (child in children) {
removeDuplicates(child)
}
}
private fun createTree(codebase: MergedCodebase, filter: Predicate<Item>? = null): List<ItemTree> {
return createTree(codebase.children, filter)
}
private fun createTree(codebase: Codebase, filter: Predicate<Item>? = null): List<ItemTree> {
return createTree(listOf(codebase), filter)
}
private fun createTree(codebases: List<Codebase>, filter: Predicate<Item>? = null): List<ItemTree> {
val stack = Stack<ItemTree>()
val root = ItemTree(null)
stack.push(root)
for (codebase in codebases) {
val acceptAll = codebase.preFiltered || filter == null
val predicate = if (acceptAll) Predicate { true } else filter!!
codebase.accept(object : ApiVisitor(
nestInnerClasses = true,
inlineInheritedFields = true,
filterEmit = predicate,
filterReference = predicate,
// Whenever a caller passes arguments of "--show-annotation 'SomeAnnotation' --check-compatibility:api:released $oldApi",
// really what they mean is:
// 1. Definitions:
// 1.1 Define the SomeAnnotation API as the set of APIs that are either public or are annotated with @SomeAnnotation
// 1.2 $oldApi was previously the difference between the SomeAnnotation api and the public api
// 2. The caller would like Metalava to verify that all APIs that are known to have previously been part of the SomeAnnotation api remain part of the SomeAnnotation api
// So, when doing compatibility checking we want to consider public APIs even if the caller didn't explicitly pass --show-unannotated
showUnannotated = true
) {
override fun visitItem(item: Item) {
val node = ItemTree(item)
val parent = stack.peek()
parent.children += node
stack.push(node)
}
override fun include(cls: ClassItem): Boolean = if (acceptAll) true else super.include(cls)
/** Include all classes in the tree, even implicitly defined classes (such as containing classes) */
override fun shouldEmitClass(vc: VisitCandidate): Boolean = true
override fun afterVisitItem(item: Item) {
stack.pop()
}
})
}
if (codebases.count() >= 2) {
removeDuplicates(root)
// removeDuplicates will also sort the items
} else {
ensureSorted(root)
}
return root.children
}
data class ItemTree(val item: Item?) : Comparable<ItemTree> {
val children: MutableList<ItemTree> = mutableListOf()
fun item(): Item = item!! // Only the root note can be null, and this method should never be called on it
override fun compareTo(other: ItemTree): Int {
return comparator.compare(item(), other.item())
}
override fun toString(): String {
return item.toString()
}
@Suppress("unused") // Left for debugging
fun prettyPrint(): String {
val sb = StringBuilder(1000)
prettyPrint(sb, 0)
return sb.toString()
}
private fun prettyPrint(sb: StringBuilder, depth: Int) {
for (i in 0 until depth) {
sb.append(" ")
}
sb.append(toString())
sb.append('\n')
for (child in children) {
child.prettyPrint(sb, depth + 1)
}
}
companion object {
@Suppress("unused") // Left for debugging
fun prettyPrint(list: List<ItemTree>): String {
val sb = StringBuilder(1000)
for (child in list) {
child.prettyPrint(sb, 0)
}
return sb.toString()
}
}
}
}