blob: 0cd51dd204db4ec4b8c248e9809cc85dd83eddd4 [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.NullnessMigration.Companion.findNullnessAnnotation
import com.android.tools.metalava.NullnessMigration.Companion.isNullable
import com.android.tools.metalava.doclava1.ApiPredicate
import com.android.tools.metalava.doclava1.Issues
import com.android.tools.metalava.doclava1.Issues.Issue
import com.android.tools.metalava.doclava1.TextCodebase
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.FieldItem
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.Item.Companion.describe
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.TypeItem
import com.android.tools.metalava.model.configuration
import com.intellij.psi.PsiField
import java.io.File
import java.util.function.Predicate
/**
* Compares the current API with a previous version and makes sure
* the changes are compatible. For example, you can make a previously
* nullable parameter non null, but not vice versa.
*
* TODO: Only allow nullness changes on final classes!
*/
class CompatibilityCheck(
val filterReference: Predicate<Item>,
private val oldCodebase: Codebase,
private val apiType: ApiType,
private val base: Codebase? = null
) : ComparisonVisitor() {
/**
* Request for compatibility checks.
* [file] represents the signature file to be checked. [apiType] represents which
* part of the API should be checked, [releaseType] represents what kind of codebase
* we are comparing it against. If [codebase] is specified, compare the signature file
* against the codebase instead of metalava's current source tree configured via the
* normal source path flags.
*/
data class CheckRequest(
val file: File,
val apiType: ApiType,
val releaseType: ReleaseType,
val codebase: File? = null
) {
override fun toString(): String {
return "--check-compatibility:${apiType.flagName}:${releaseType.flagName} $file"
}
}
/** In old signature files, methods inherited from hidden super classes
* are not included. An example of this is StringBuilder.setLength.
* More details about this are listed in Compatibility.skipInheritedMethods.
* We may see these in the codebase but not in the (old) signature files,
* so in these cases we want to ignore certain changes such as considering
* StringBuilder.setLength a newly added method.
*/
private val comparingWithPartialSignatures = oldCodebase is TextCodebase && oldCodebase.format == FileFormat.V1
var foundProblems = false
override fun compare(old: Item, new: Item) {
val oldModifiers = old.modifiers
val newModifiers = new.modifiers
if (oldModifiers.isOperator() && !newModifiers.isOperator()) {
report(
Issues.OPERATOR_REMOVAL,
new,
"Cannot remove `operator` modifier from ${describe(new)}: Incompatible change"
)
}
if (oldModifiers.isInfix() && !newModifiers.isInfix()) {
report(
Issues.INFIX_REMOVAL,
new,
"Cannot remove `infix` modifier from ${describe(new)}: Incompatible change"
)
}
// Should not remove nullness information
// Can't change information incompatibly
val oldNullnessAnnotation = findNullnessAnnotation(old)
if (oldNullnessAnnotation != null) {
val newNullnessAnnotation = findNullnessAnnotation(new)
if (newNullnessAnnotation == null) {
val implicitNullness = AnnotationItem.getImplicitNullness(new)
if (implicitNullness == true && isNullable(old)) {
return
}
if (implicitNullness == false && !isNullable(old)) {
return
}
val name = AnnotationItem.simpleName(oldNullnessAnnotation)
if (old.type()?.primitive == true) {
return
}
report(
Issues.INVALID_NULL_CONVERSION, new,
"Attempted to remove $name annotation from ${describe(new)}"
)
} else {
val oldNullable = isNullable(old)
val newNullable = isNullable(new)
if (oldNullable != newNullable) {
// You can change a parameter from nonnull to nullable
// You can change a method from nullable to nonnull
// You cannot change a parameter from nullable to nonnull
// You cannot change a method from nonnull to nullable
if (oldNullable && old is ParameterItem) {
report(
Issues.INVALID_NULL_CONVERSION,
new,
"Attempted to change parameter from @Nullable to @NonNull: " +
"incompatible change for ${describe(new)}"
)
} else if (!oldNullable && old is MethodItem) {
report(
Issues.INVALID_NULL_CONVERSION,
new,
"Attempted to change method return from @NonNull to @Nullable: " +
"incompatible change for ${describe(new)}"
)
}
}
}
}
}
override fun compare(old: ParameterItem, new: ParameterItem) {
val prevName = old.publicName() ?: return
val newName = new.publicName()
if (newName == null) {
report(
Issues.PARAMETER_NAME_CHANGE,
new,
"Attempted to remove parameter name from ${describe(new)} in ${describe(new.containingMethod())}"
)
} else if (newName != prevName) {
report(
Issues.PARAMETER_NAME_CHANGE,
new,
"Attempted to change parameter name from $prevName to $newName in ${describe(new.containingMethod())}"
)
}
if (old.hasDefaultValue() && !new.hasDefaultValue()) {
report(
Issues.DEFAULT_VALUE_CHANGE,
new,
"Attempted to remove default value from ${describe(new)} in ${describe(new.containingMethod())}"
)
}
if (old.isVarArgs() && !new.isVarArgs()) {
// In Java, changing from array to varargs is a compatible change, but
// not the other way around. Kotlin is the same, though in Kotlin
// you have to change the parameter type as well to an array type; assuming you
// do that it's the same situation as Java; otherwise the normal
// signature check will catch the incompatibility.
report(
Issues.VARARG_REMOVAL,
new,
"Changing from varargs to array is an incompatible change: ${describe(
new,
includeParameterTypes = true,
includeParameterNames = true
)}"
)
}
}
override fun compare(old: ClassItem, new: ClassItem) {
val oldModifiers = old.modifiers
val newModifiers = new.modifiers
if (old.isInterface() != new.isInterface()) {
report(
Issues.CHANGED_CLASS, new, "${describe(new, capitalize = true)} changed class/interface declaration"
)
return // Avoid further warnings like "has changed abstract qualifier" which is implicit in this change
}
for (iface in old.interfaceTypes()) {
val qualifiedName = iface.asClass()?.qualifiedName() ?: continue
if (!new.implements(qualifiedName)) {
report(
Issues.REMOVED_INTERFACE, new, "${describe(old, capitalize = true)} no longer implements $iface"
)
}
}
for (iface in new.filteredInterfaceTypes(filterReference)) {
val qualifiedName = iface.asClass()?.qualifiedName() ?: continue
if (!old.implements(qualifiedName)) {
report(
Issues.ADDED_INTERFACE, new, "Added interface $iface to class ${describe(old)}"
)
}
}
if (!oldModifiers.isSealed() && newModifiers.isSealed()) {
report(Issues.ADD_SEALED, new, "Cannot add 'sealed' modifier to ${describe(new)}: Incompatible change")
} else if (old.isClass() && oldModifiers.isAbstract() != newModifiers.isAbstract()) {
report(
Issues.CHANGED_ABSTRACT, new, "${describe(new, capitalize = true)} changed 'abstract' qualifier"
)
}
// Check for changes in final & static, but not in enums (since PSI and signature files differ
// a bit in whether they include these for enums
if (!new.isEnum()) {
if (!oldModifiers.isFinal() && newModifiers.isFinal()) {
// It is safe to make a class final if it did not previously have any public
// constructors because it was impossible for an application to create a subclass.
if (old.constructors().filter { it.isPublic || it.isProtected }.none()) {
report(
Issues.ADDED_FINAL_UNINSTANTIABLE, new,
"${describe(
new,
capitalize = true
)} added 'final' qualifier but was previously uninstantiable and therefore could not be subclassed"
)
} else {
report(
Issues.ADDED_FINAL, new, "${describe(new, capitalize = true)} added 'final' qualifier"
)
}
} else if (oldModifiers.isFinal() && !newModifiers.isFinal()) {
report(
Issues.REMOVED_FINAL, new, "${describe(new, capitalize = true)} removed 'final' qualifier"
)
}
if (oldModifiers.isStatic() != newModifiers.isStatic()) {
val hasPublicConstructor = old.constructors().any { it.isPublic }
if (!old.isInnerClass() || hasPublicConstructor) {
report(
Issues.CHANGED_STATIC,
new,
"${describe(new, capitalize = true)} changed 'static' qualifier"
)
}
}
}
val oldVisibility = oldModifiers.getVisibilityString()
val newVisibility = newModifiers.getVisibilityString()
if (oldVisibility != newVisibility) {
// TODO: Use newModifiers.asAccessibleAs(oldModifiers) to provide different error messages
// based on whether this seems like a reasonable change, e.g. making a private or final method more
// accessible is fine (no overridden method affected) but not making methods less accessible etc
report(
Issues.CHANGED_SCOPE, new,
"${describe(new, capitalize = true)} changed visibility from $oldVisibility to $newVisibility"
)
}
if (!old.deprecated == new.deprecated) {
report(
Issues.CHANGED_DEPRECATED, new,
"${describe(
new,
capitalize = true
)} has changed deprecation state ${old.deprecated} --> ${new.deprecated}"
)
}
val oldSuperClassName = old.superClass()?.qualifiedName()
if (oldSuperClassName != null) { // java.lang.Object can't have a superclass.
if (!new.extends(oldSuperClassName)) {
report(
Issues.CHANGED_SUPERCLASS, new,
"${describe(
new,
capitalize = true
)} superclass changed from $oldSuperClassName to ${new.superClass()?.qualifiedName()}"
)
}
}
if (old.hasTypeVariables() && new.hasTypeVariables()) {
val oldTypeParamsCount = old.typeParameterList().typeParameterCount()
val newTypeParamsCount = new.typeParameterList().typeParameterCount()
if (oldTypeParamsCount != newTypeParamsCount) {
report(
Issues.CHANGED_TYPE, new,
"${describe(
old,
capitalize = true
)} changed number of type parameters from $oldTypeParamsCount to $newTypeParamsCount"
)
}
}
}
override fun compare(old: MethodItem, new: MethodItem) {
val oldModifiers = old.modifiers
val newModifiers = new.modifiers
val oldReturnType = old.returnType()
val newReturnType = new.returnType()
if (!new.isConstructor() && oldReturnType != null && newReturnType != null) {
val oldTypeParameter = oldReturnType.asTypeParameter(old)
val newTypeParameter = newReturnType.asTypeParameter(new)
var compatible = true
if (oldTypeParameter == null &&
newTypeParameter == null
) {
if (oldReturnType != newReturnType ||
oldReturnType.arrayDimensions() != newReturnType.arrayDimensions()
) {
compatible = false
}
} else if (oldTypeParameter == null && newTypeParameter != null) {
val constraints = newTypeParameter.bounds()
for (constraint in constraints) {
val oldClass = oldReturnType.asClass()
if (oldClass == null || !oldClass.extendsOrImplements(constraint.qualifiedName())) {
compatible = false
}
}
} else if (oldTypeParameter != null && newTypeParameter == null) {
// It's never valid to go from being a parameterized type to not being one.
// This would drop the implicit cast breaking backwards compatibility.
compatible = false
} else {
// If both return types are parameterized then the constraints must be
// exactly the same.
val oldConstraints = oldTypeParameter?.bounds() ?: emptyList()
val newConstraints = newTypeParameter?.bounds() ?: emptyList()
if (oldConstraints.size != newConstraints.size ||
newConstraints != oldConstraints
) {
val oldTypeString = describeBounds(oldReturnType, oldConstraints)
val newTypeString = describeBounds(newReturnType, newConstraints)
val message =
"${describe(
new,
capitalize = true
)} has changed return type from $oldTypeString to $newTypeString"
report(Issues.CHANGED_TYPE, new, message)
return
}
}
if (!compatible) {
var oldTypeString = oldReturnType.toSimpleType()
var newTypeString = newReturnType.toSimpleType()
// Typically, show short type names like "String" if they're distinct (instead of long type names like
// "java.util.Set<T!>")
if (oldTypeString == newTypeString) {
// If the short names aren't unique, then show full type names like "java.util.Set<T!>"
oldTypeString = oldReturnType.toString()
newTypeString = newReturnType.toString()
}
val message =
"${describe(new, capitalize = true)} has changed return type from $oldTypeString to $newTypeString"
report(Issues.CHANGED_TYPE, new, message)
}
// Annotation methods?
if (!old.hasSameValue(new)) {
val prevValue = old.defaultValue()
val prevString = if (prevValue.isEmpty()) {
"nothing"
} else {
prevValue
}
val newValue = new.defaultValue()
val newString = if (newValue.isEmpty()) {
"nothing"
} else {
newValue
}
val message = "${describe(
new,
capitalize = true
)} has changed value from $prevString to $newString"
report(Issues.CHANGED_VALUE, new, message)
}
}
// Check for changes in abstract, but only for regular classes; older signature files
// sometimes describe interface methods as abstract
if (new.containingClass().isClass()) {
if (!oldModifiers.isAbstract() && newModifiers.isAbstract() &&
// In old signature files, overridden methods of abstract methods declared
// in super classes are sometimes omitted by doclava. This means that the method
// looks (from the signature file perspective) like it has not been implemented,
// whereas in reality it has. For just one example of this, consider
// FragmentBreadCrumbs.onLayout: it's a concrete implementation in that class
// of the inherited method from ViewGroup. However, in the signature file,
// FragmentBreadCrumbs does not list this method; it's only listed (as abstract)
// in the super class. In this scenario, the compatibility check would believe
// the old method in FragmentBreadCrumbs is abstract and the new method is not,
// which is not the case. Therefore, if the old method is coming from a signature
// file based codebase with an old format, we omit abstract change warnings.
// The reverse situation can also happen: AbstractSequentialList defines listIterator
// as abstract, but it's not recorded as abstract in the signature files anywhere,
// so we treat this as a nearly abstract method, which it is not.
(old.inheritedFrom == null || !comparingWithPartialSignatures)
) {
report(
Issues.CHANGED_ABSTRACT, new, "${describe(new, capitalize = true)} has changed 'abstract' qualifier"
)
}
}
if (oldModifiers.isNative() != newModifiers.isNative()) {
report(
Issues.CHANGED_NATIVE, new, "${describe(new, capitalize = true)} has changed 'native' qualifier"
)
}
// Check changes to final modifier. But skip enums where it varies between signature files and PSI
// whether the methods are considered final.
if (!new.containingClass().isEnum() && !oldModifiers.isStatic()) {
// Skip changes in final; modifier change could come from inherited
// implementation from hidden super class. An example of this
// is SpannableString.charAt whose implementation comes from
// SpannableStringInternal.
if (old.inheritedFrom == null || !comparingWithPartialSignatures) {
// Compiler-generated methods vary in their 'final' qualifier between versions of
// the compiler, so this check needs to be quite narrow. A change in 'final'
// status of a method is only relevant if (a) the method is not declared 'static'
// and (b) the method is not already inferred to be 'final' by virtue of its class.
if (!old.isEffectivelyFinal() && new.isEffectivelyFinal()) {
report(
Issues.ADDED_FINAL, new, "${describe(new, capitalize = true)} has added 'final' qualifier"
)
} else if (old.isEffectivelyFinal() && !new.isEffectivelyFinal()) {
report(
Issues.REMOVED_FINAL, new, "${describe(new, capitalize = true)} has removed 'final' qualifier"
)
}
}
}
if (oldModifiers.isStatic() != newModifiers.isStatic()) {
report(
Issues.CHANGED_STATIC, new, "${describe(new, capitalize = true)} has changed 'static' qualifier"
)
}
val oldVisibility = oldModifiers.getVisibilityString()
val newVisibility = newModifiers.getVisibilityString()
if (oldVisibility != newVisibility) {
// TODO: Use newModifiers.asAccessibleAs(oldModifiers) to provide different error messages
// based on whether this seems like a reasonable change, e.g. making a private or final method more
// accessible is fine (no overridden method affected) but not making methods less accessible etc
report(
Issues.CHANGED_SCOPE, new,
"${describe(new, capitalize = true)} changed visibility from $oldVisibility to $newVisibility"
)
}
if (old.deprecated != new.deprecated) {
report(
Issues.CHANGED_DEPRECATED, new,
"${describe(
new,
capitalize = true
)} has changed deprecation state ${old.deprecated} --> ${new.deprecated}"
)
}
/*
// see JLS 3 13.4.20 "Adding or deleting a synchronized modifier of a method does not break "
// "compatibility with existing binaries."
if (oldModifiers.isSynchronized() != newModifiers.isSynchronized()) {
report(
Errors.CHANGED_SYNCHRONIZED, new,
"${describe(
new,
capitalize = true
)} has changed 'synchronized' qualifier from ${oldModifiers.isSynchronized()} to ${newModifiers.isSynchronized()}"
)
}
*/
for (exception in old.throwsTypes()) {
if (!new.throws(exception.qualifiedName())) {
// exclude 'throws' changes to finalize() overrides with no arguments
if (old.name() != "finalize" || old.parameters().isNotEmpty()) {
report(
Issues.CHANGED_THROWS, new,
"${describe(new, capitalize = true)} no longer throws exception ${exception.qualifiedName()}"
)
}
}
}
for (exec in new.filteredThrowsTypes(filterReference)) {
// exclude 'throws' changes to finalize() overrides with no arguments
if (!old.throws(exec.qualifiedName())) {
if (old.name() != "finalize" || old.parameters().isNotEmpty()) {
val message = "${describe(new, capitalize = true)} added thrown exception ${exec.qualifiedName()}"
report(Issues.CHANGED_THROWS, new, message)
}
}
}
if (new.modifiers.isInline()) {
val oldTypes = old.typeParameterList().typeParameters()
val newTypes = new.typeParameterList().typeParameters()
for (i in 0 until oldTypes.size) {
if (i == newTypes.size) {
break
}
if (newTypes[i].isReified() && !oldTypes[i].isReified()) {
val message = "${describe(
new,
capitalize = true
)} made type variable ${newTypes[i].simpleName()} reified: incompatible change"
report(Issues.CHANGED_THROWS, new, message)
}
}
}
}
private fun describeBounds(
type: TypeItem,
constraints: List<ClassItem>
): String {
return type.toSimpleType() +
if (constraints.isEmpty()) {
" (extends java.lang.Object)"
} else {
" (extends ${constraints.joinToString(separator = " & ") { it.qualifiedName() }})"
}
}
override fun compare(old: FieldItem, new: FieldItem) {
val oldModifiers = old.modifiers
val newModifiers = new.modifiers
if (!old.isEnumConstant()) {
val oldType = old.type()
val newType = new.type()
if (oldType != newType) {
val message = "${describe(new, capitalize = true)} has changed type from $oldType to $newType"
report(Issues.CHANGED_TYPE, new, message)
} else if (!old.hasSameValue(new)) {
val prevValue = old.initialValue(true)
val prevString = if (prevValue == null && !old.modifiers.isFinal()) {
"nothing/not constant"
} else {
prevValue
}
val newValue = new.initialValue(true)
val newString = if (newValue is PsiField) {
newValue.containingClass?.qualifiedName + "." + newValue.name
} else {
newValue
}
val message = "${describe(
new,
capitalize = true
)} has changed value from $prevString to $newString"
if (message == "Field android.telephony.data.ApnSetting.TYPE_DEFAULT has changed value from 17 to 1") {
// Temporarily ignore: this value changed incompatibly from 28.txt to current.txt.
// It's not clear yet whether this value change needs to be reverted, or suppressed
// permanently in the source code, but suppressing from metalava so we can unblock
// getting the compatibility checks enabled.
} else
report(Issues.CHANGED_VALUE, new, message)
}
}
val oldVisibility = oldModifiers.getVisibilityString()
val newVisibility = newModifiers.getVisibilityString()
if (oldVisibility != newVisibility) {
// TODO: Use newModifiers.asAccessibleAs(oldModifiers) to provide different error messages
// based on whether this seems like a reasonable change, e.g. making a private or final method more
// accessible is fine (no overridden method affected) but not making methods less accessible etc
report(
Issues.CHANGED_SCOPE, new,
"${describe(new, capitalize = true)} changed visibility from $oldVisibility to $newVisibility"
)
}
if (oldModifiers.isStatic() != newModifiers.isStatic()) {
report(
Issues.CHANGED_STATIC, new, "${describe(new, capitalize = true)} has changed 'static' qualifier"
)
}
if (!oldModifiers.isFinal() && newModifiers.isFinal()) {
report(
Issues.ADDED_FINAL, new, "${describe(new, capitalize = true)} has added 'final' qualifier"
)
} else if (oldModifiers.isFinal() && !newModifiers.isFinal()) {
report(
Issues.REMOVED_FINAL, new, "${describe(new, capitalize = true)} has removed 'final' qualifier"
)
}
if (oldModifiers.isTransient() != newModifiers.isTransient()) {
report(
Issues.CHANGED_TRANSIENT, new, "${describe(new, capitalize = true)} has changed 'transient' qualifier"
)
}
if (oldModifiers.isVolatile() != newModifiers.isVolatile()) {
report(
Issues.CHANGED_VOLATILE, new, "${describe(new, capitalize = true)} has changed 'volatile' qualifier"
)
}
if (old.deprecated != new.deprecated) {
report(
Issues.CHANGED_DEPRECATED, new,
"${describe(
new,
capitalize = true
)} has changed deprecation state ${old.deprecated} --> ${new.deprecated}"
)
}
}
private fun handleAdded(issue: Issue, item: Item) {
if (item.originallyHidden) {
// This is an element which is hidden but is referenced from
// some public API. This is an error, but some existing code
// is doing this. This is not an API addition.
return
}
var message = "Added ${describe(item)}"
// Clarify error message for removed API to make it less ambiguous
if (apiType == ApiType.REMOVED) {
message += " to the removed API"
} else if (options.showAnnotations.isNotEmpty()) {
if (options.showAnnotations.matchesSuffix("SystemApi")) {
message += " to the system API"
} else if (options.showAnnotations.matchesSuffix("TestApi")) {
message += " to the test API"
}
}
// In some cases we run the comparison on signature files
// generated into the temp directory, but in these cases
// try to report the item against the real item in the API instead
val equivalent = findBaseItem(item)
if (equivalent != null) {
report(issue, equivalent, message)
return
}
report(issue, item, message)
}
private fun handleRemoved(issue: Issue, item: Item) {
if (!item.emit) {
// It's a stub; this can happen when analyzing partial APIs
// such as a signature file for a library referencing types
// from the upstream library dependencies.
return
}
if (base != null) {
// We're diffing "overlay" APIs, such as system or test API files,
// where the signature files only list a delta from the full, "base" API.
// In that case, if an API is promoted from @SystemApi or @TestApi to be
// a full part of the API, it will look like a removal; it appeared in the
// previous file and not in the new file, but it's not removed, it's just
// not a delta anymore.
//
// For that reason, we also pass in the "base" API in these cases, and when
// an item is removed, we also check the full API to see if it's present
// there, and if so, this item is not actually deleted.
val baseItem = findBaseItem(item)
if (baseItem != null && ApiPredicate(ignoreShown = true).test(baseItem)) {
return
}
}
report(issue, item, "Removed ${if (item.deprecated) "deprecated " else ""}${describe(item)}")
}
private fun findBaseItem(
item: Item
): Item? {
base ?: return null
return when (item) {
is PackageItem -> base.findPackage(item.qualifiedName())
is ClassItem -> base.findClass(item.qualifiedName())
is MethodItem -> base.findClass(item.containingClass().qualifiedName())?.findMethod(
item,
true,
true
)
is FieldItem -> base.findClass(item.containingClass().qualifiedName())?.findField(item.name())
else -> null
}
}
override fun added(new: PackageItem) {
handleAdded(Issues.ADDED_PACKAGE, new)
}
override fun added(new: ClassItem) {
val error = if (new.isInterface()) {
Issues.ADDED_INTERFACE
} else {
Issues.ADDED_CLASS
}
handleAdded(error, new)
}
override fun added(new: MethodItem) {
// In old signature files, methods inherited from hidden super classes
// are not included. An example of this is StringBuilder.setLength.
// More details about this are listed in Compatibility.skipInheritedMethods.
// We may see these in the codebase but not in the (old) signature files,
// so skip these -- they're not really "added".
if (new.inheritedFrom != null && comparingWithPartialSignatures) {
return
}
// *Overriding* methods from super classes that are outside the
// API is OK (e.g. overriding toString() from java.lang.Object)
val superMethods = new.superMethods()
for (superMethod in superMethods) {
if (superMethod.isFromClassPath()) {
return
}
}
// Do not fail if this "new" method is really an override of an
// existing superclass method, but we should fail if this is overriding
// an abstract method, because method's abstractness affects how users use it.
// See if there's a member from inherited class
val inherited = if (new.isConstructor()) {
null
} else {
new.containingClass().findMethod(
new,
includeSuperClasses = true,
includeInterfaces = false
)
}
// Builtin annotation methods: just a difference in signature file
if ((new.name() == "values" && new.parameters().isEmpty() || new.name() == "valueOf" &&
new.parameters().size == 1) && new.containingClass().isEnum()
) {
return
}
// In old signature files, annotation methods are missing! This will show up as an added method.
if (new.containingClass().isAnnotationType() && oldCodebase is TextCodebase && oldCodebase.format == FileFormat.V1) {
return
}
// In most cases it is not permitted to add a new method to an interface, even with a
// default implementation because it could could create ambiguity if client code implements
// two interfaces that each now define methods with the same signature.
// Annotation types cannot implement other interfaces, however, so it is permitted to add
// add new default methods to annotation types.
if (new.containingClass().isAnnotationType() && new.hasDefaultValue()) {
return
}
if (inherited == null || inherited == new || !inherited.modifiers.isAbstract()) {
val error = if (new.modifiers.isAbstract()) Issues.ADDED_ABSTRACT_METHOD else Issues.ADDED_METHOD
handleAdded(error, new)
}
}
override fun added(new: FieldItem) {
if (new.inheritedFrom != null && comparingWithPartialSignatures) {
return
}
handleAdded(Issues.ADDED_FIELD, new)
}
override fun removed(old: PackageItem, from: Item?) {
handleRemoved(Issues.REMOVED_PACKAGE, old)
}
override fun removed(old: ClassItem, from: Item?) {
val error = when {
old.isInterface() -> Issues.REMOVED_INTERFACE
old.deprecated -> Issues.REMOVED_DEPRECATED_CLASS
else -> Issues.REMOVED_CLASS
}
handleRemoved(error, old)
}
override fun removed(old: MethodItem, from: ClassItem?) {
// See if there's a member from inherited class
val inherited = if (old.isConstructor()) {
null
} else {
// This can also return self, specially handled below
from?.findMethod(
old,
includeSuperClasses = true,
includeInterfaces = from.isInterface()
)
}
if (inherited == null || inherited != old && inherited.isHiddenOrRemoved()) {
val error = if (old.deprecated) Issues.REMOVED_DEPRECATED_METHOD else Issues.REMOVED_METHOD
handleRemoved(error, old)
}
}
override fun removed(old: FieldItem, from: ClassItem?) {
val inherited = from?.findField(
old.name(),
includeSuperClasses = true,
includeInterfaces = from.isInterface()
)
if (inherited == null) {
val error = if (old.deprecated) Issues.REMOVED_DEPRECATED_FIELD else Issues.REMOVED_FIELD
handleRemoved(error, old)
}
}
private fun report(
issue: Issue,
item: Item,
message: String
) {
if (reporter.report(issue, item, message) && configuration.getSeverity(issue) == Severity.ERROR) {
foundProblems = true
}
}
companion object {
fun checkCompatibility(
codebase: Codebase,
previous: Codebase,
releaseType: ReleaseType,
apiType: ApiType,
base: Codebase? = null
) {
val filter = apiType.getEmitFilter()
val checker = CompatibilityCheck(filter, previous, apiType, base)
val errorConfiguration = releaseType.getErrorConfiguration()
val previousConfiguration = configuration
try {
configuration = errorConfiguration
CodebaseComparator().compare(checker, previous, codebase, filter)
} finally {
configuration = previousConfiguration
}
val message = "Aborting: Found compatibility problems checking " +
"the ${apiType.displayName} API against the API in ${previous.location}"
if (checker.foundProblems) {
throw DriverException(exitCode = -1, stderr = message)
}
}
}
}