1 /* <lambda>null2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tools.metalava.compatibility 18 19 import com.android.tools.metalava.CodebaseComparator 20 import com.android.tools.metalava.ComparisonVisitor 21 import com.android.tools.metalava.JVM_DEFAULT_WITH_COMPATIBILITY 22 import com.android.tools.metalava.cli.common.cliError 23 import com.android.tools.metalava.model.ANDROID_SYSTEM_API 24 import com.android.tools.metalava.model.ANDROID_TEST_API 25 import com.android.tools.metalava.model.ArrayTypeItem 26 import com.android.tools.metalava.model.CallableItem 27 import com.android.tools.metalava.model.ClassItem 28 import com.android.tools.metalava.model.ClassOrigin 29 import com.android.tools.metalava.model.Codebase 30 import com.android.tools.metalava.model.FieldItem 31 import com.android.tools.metalava.model.FilterPredicate 32 import com.android.tools.metalava.model.Item 33 import com.android.tools.metalava.model.Item.Companion.describe 34 import com.android.tools.metalava.model.ItemLanguage 35 import com.android.tools.metalava.model.MergedCodebase 36 import com.android.tools.metalava.model.MethodItem 37 import com.android.tools.metalava.model.MultipleTypeVisitor 38 import com.android.tools.metalava.model.PackageItem 39 import com.android.tools.metalava.model.ParameterItem 40 import com.android.tools.metalava.model.SelectableItem 41 import com.android.tools.metalava.model.TypeItem 42 import com.android.tools.metalava.model.TypeNullability 43 import com.android.tools.metalava.model.VariableTypeItem 44 import com.android.tools.metalava.model.visitors.ApiType 45 import com.android.tools.metalava.options 46 import com.android.tools.metalava.reporter.FileLocation 47 import com.android.tools.metalava.reporter.IssueConfiguration 48 import com.android.tools.metalava.reporter.Issues 49 import com.android.tools.metalava.reporter.Issues.Issue 50 import com.android.tools.metalava.reporter.Reporter 51 import com.android.tools.metalava.reporter.Severity 52 import com.intellij.psi.PsiField 53 54 /** 55 * Compares the current API with a previous version and makes sure the changes are compatible. For 56 * example, you can make a previously nullable parameter non null, but not vice versa. 57 */ 58 class CompatibilityCheck( 59 val filterReference: FilterPredicate, 60 private val apiType: ApiType, 61 private val reporter: Reporter, 62 private val issueConfiguration: IssueConfiguration, 63 private val apiCompatAnnotations: Set<String>, 64 ) : ComparisonVisitor() { 65 66 var foundProblems = false 67 68 private fun possibleContainingMethod(item: Item): MethodItem? { 69 if (item is MethodItem) { 70 return item 71 } 72 if (item is ParameterItem) { 73 return item.possibleContainingMethod() 74 } 75 return null 76 } 77 78 private fun compareItemNullability(old: Item, new: Item) { 79 val oldMethod = possibleContainingMethod(old) 80 val newMethod = possibleContainingMethod(new) 81 82 if (oldMethod != null && newMethod != null) { 83 if ( 84 oldMethod.containingClass().qualifiedName() != 85 newMethod.containingClass().qualifiedName() || 86 (oldMethod.inheritedFromAncestor != newMethod.inheritedFromAncestor) 87 ) { 88 // If the old method and new method are defined on different classes, then it's 89 // possible that the old method was previously overridden and we omitted it. 90 // So, if the old method and new methods are defined on different classes, then we 91 // skip nullability checks 92 return 93 } 94 } 95 96 // In a final method, you can change a parameter from nonnull to nullable. 97 // This will also allow a constructor parameter to be changed from nonnull to nullable if 98 // the class is not extensible. 99 // TODO: Allow the parameter of any constructor to be switched from nonnull to nullable as 100 // they can never be overridden. 101 val allowNonNullToNullable = 102 new is ParameterItem && !new.containingCallable().canBeExternallyOverridden() 103 // In a final method, you can change a method return from nullable to nonnull 104 val allowNullableToNonNull = new is MethodItem && !new.canBeExternallyOverridden() 105 106 old.type() 107 ?.accept( 108 object : MultipleTypeVisitor() { 109 override fun visitType(type: TypeItem, other: List<TypeItem>) { 110 val newType = other.singleOrNull() ?: return 111 compareTypeNullability( 112 type, 113 newType, 114 new, 115 allowNonNullToNullable, 116 allowNullableToNonNull, 117 ) 118 } 119 }, 120 listOfNotNull(new.type()) 121 ) 122 } 123 124 private fun compareTypeNullability( 125 old: TypeItem, 126 new: TypeItem, 127 context: Item, 128 allowNonNullToNullable: Boolean, 129 allowNullableToNonNull: Boolean, 130 ) { 131 // Should not remove nullness information 132 // Can't change information incompatibly 133 val oldNullability = old.modifiers.nullability 134 val newNullability = new.modifiers.nullability 135 if ( 136 (oldNullability == TypeNullability.NONNULL || 137 oldNullability == TypeNullability.NULLABLE) && 138 newNullability == TypeNullability.PLATFORM 139 ) { 140 report( 141 Issues.INVALID_NULL_CONVERSION, 142 context, 143 "Attempted to remove nullability from ${new.toTypeString()} (was $oldNullability) in ${describe(context)}" 144 ) 145 } else if (oldNullability != newNullability) { 146 if ( 147 (oldNullability == TypeNullability.NULLABLE && 148 newNullability == TypeNullability.NONNULL && 149 !allowNullableToNonNull) || 150 (oldNullability == TypeNullability.NONNULL && 151 newNullability == TypeNullability.NULLABLE && 152 !allowNonNullToNullable) 153 ) { 154 // This check used to be more permissive. To transition to a stronger check, use 155 // WARNING_ERROR_WHEN_NEW if the change used to be allowed. 156 val previouslyAllowed = 157 (oldNullability == TypeNullability.NULLABLE && context is MethodItem) || 158 ((oldNullability == TypeNullability.NONNULL && context is ParameterItem)) 159 val maximumSeverity = 160 if (previouslyAllowed) { 161 Severity.WARNING_ERROR_WHEN_NEW 162 } else { 163 Severity.ERROR 164 } 165 report( 166 Issues.INVALID_NULL_CONVERSION, 167 context, 168 "Attempted to change nullability of ${new.toTypeString()} (from $oldNullability to $newNullability) in ${describe(context)}", 169 maximumSeverity = maximumSeverity, 170 ) 171 } 172 } 173 } 174 175 override fun compareItems(old: Item, new: Item) { 176 val oldModifiers = old.modifiers 177 val newModifiers = new.modifiers 178 if (oldModifiers.isOperator() && !newModifiers.isOperator()) { 179 report( 180 Issues.OPERATOR_REMOVAL, 181 new, 182 "Cannot remove `operator` modifier from ${describe(new)}: Incompatible change" 183 ) 184 } 185 186 if (oldModifiers.isInfix() && !newModifiers.isInfix()) { 187 report( 188 Issues.INFIX_REMOVAL, 189 new, 190 "Cannot remove `infix` modifier from ${describe(new)}: Incompatible change" 191 ) 192 } 193 194 if (!old.isCompatibilitySuppressed() && new.isCompatibilitySuppressed()) { 195 report( 196 Issues.BECAME_UNCHECKED, 197 old, 198 "Removed ${describe(old)} from compatibility checked API surface" 199 ) 200 } 201 202 apiCompatAnnotations.forEach { annotation -> 203 val isOldAnnotated = oldModifiers.isAnnotatedWith(annotation) 204 val newAnnotation = newModifiers.findAnnotation(annotation) 205 if (isOldAnnotated && newAnnotation == null) { 206 report( 207 Issues.REMOVED_ANNOTATION, 208 new, 209 "Cannot remove @$annotation annotation from ${describe(old)}: Incompatible change", 210 ) 211 } else if (!isOldAnnotated && newAnnotation != null) { 212 report( 213 Issues.ADDED_ANNOTATION, 214 new, 215 "Cannot add @$annotation annotation to ${describe(old)}: Incompatible change", 216 newAnnotation.fileLocation, 217 ) 218 } 219 } 220 221 compareItemNullability(old, new) 222 } 223 224 override fun compareParameterItems(old: ParameterItem, new: ParameterItem) { 225 val prevName = old.publicName() 226 val newName = new.publicName() 227 if (prevName != null) { 228 if (newName == null) { 229 report( 230 Issues.PARAMETER_NAME_CHANGE, 231 new, 232 "Attempted to remove parameter name from ${describe(new)}" 233 ) 234 } else if (newName != prevName) { 235 report( 236 Issues.PARAMETER_NAME_CHANGE, 237 new, 238 "Attempted to change parameter name from $prevName to $newName in ${describe(new.containingCallable())}" 239 ) 240 } 241 } 242 243 if (old.hasDefaultValue() && !new.hasDefaultValue()) { 244 report( 245 Issues.DEFAULT_VALUE_CHANGE, 246 new, 247 "Attempted to remove default value from ${describe(new)}" 248 ) 249 } 250 251 if (old.isVarArgs() && !new.isVarArgs()) { 252 // In Java, changing from array to varargs is a compatible change, but 253 // not the other way around. Kotlin is the same, though in Kotlin 254 // you have to change the parameter type as well to an array type; assuming you 255 // do that it's the same situation as Java; otherwise the normal 256 // signature check will catch the incompatibility. 257 report( 258 Issues.VARARG_REMOVAL, 259 new, 260 "Changing from varargs to array is an incompatible change: ${describe( 261 new, 262 includeParameterTypes = true, 263 includeParameterNames = true 264 )}" 265 ) 266 } 267 } 268 269 override fun compareClassItems(old: ClassItem, new: ClassItem) { 270 val oldModifiers = old.modifiers 271 val newModifiers = new.modifiers 272 273 if ( 274 old.isInterface() != new.isInterface() || 275 old.isEnum() != new.isEnum() || 276 old.isAnnotationType() != new.isAnnotationType() 277 ) { 278 report( 279 Issues.CHANGED_CLASS, 280 new, 281 "${describe(new, capitalize = true)} changed class/interface declaration" 282 ) 283 return // Avoid further warnings like "has changed abstract qualifier" which is implicit 284 // in this change 285 } 286 287 for (iface in old.interfaceTypes()) { 288 val qualifiedName = iface.asClass()?.qualifiedName() ?: continue 289 if (!new.implements(qualifiedName)) { 290 report( 291 Issues.REMOVED_INTERFACE, 292 new, 293 "${describe(old, capitalize = true)} no longer implements $iface" 294 ) 295 } 296 } 297 298 for (iface in new.filteredInterfaceTypes(filterReference)) { 299 val qualifiedName = iface.asClass()?.qualifiedName() ?: continue 300 if (!old.implements(qualifiedName)) { 301 report( 302 Issues.ADDED_INTERFACE, 303 new, 304 "Added interface $iface to class ${describe(old)}" 305 ) 306 } 307 } 308 309 if (!oldModifiers.isSealed() && newModifiers.isSealed()) { 310 report( 311 Issues.ADD_SEALED, 312 new, 313 "Cannot add 'sealed' modifier to ${describe(new)}: Incompatible change" 314 ) 315 } else if (old.isClass() && !oldModifiers.isAbstract() && newModifiers.isAbstract()) { 316 report( 317 Issues.CHANGED_ABSTRACT, 318 new, 319 "${describe(new, capitalize = true)} changed 'abstract' qualifier" 320 ) 321 } 322 323 if (oldModifiers.isFunctional() && !newModifiers.isFunctional()) { 324 report( 325 Issues.FUN_REMOVAL, 326 new, 327 "Cannot remove 'fun' modifier from ${describe(new)}: source incompatible change" 328 ) 329 } 330 331 // Check for changes in final & static, but not in enums (since PSI and signature files 332 // differ 333 // a bit in whether they include these for enums 334 if (!new.isEnum()) { 335 if (!oldModifiers.isFinal() && newModifiers.isFinal()) { 336 // It is safe to make a class final if was impossible for an application to create a 337 // subclass. 338 if (!old.isExtensible()) { 339 report( 340 Issues.ADDED_FINAL_UNINSTANTIABLE, 341 new, 342 "${ 343 describe( 344 new, 345 capitalize = true 346 ) 347 } added 'final' qualifier but was previously uninstantiable and therefore could not be subclassed" 348 ) 349 } else { 350 report( 351 Issues.ADDED_FINAL, 352 new, 353 "${describe(new, capitalize = true)} added 'final' qualifier" 354 ) 355 } 356 } 357 358 if (oldModifiers.isStatic() != newModifiers.isStatic()) { 359 val hasPublicConstructor = old.constructors().any { it.isPublic } 360 if (!old.isNestedClass() || hasPublicConstructor) { 361 report( 362 Issues.CHANGED_STATIC, 363 new, 364 "${describe(new, capitalize = true)} changed 'static' qualifier" 365 ) 366 } 367 } 368 } 369 370 val oldVisibility = oldModifiers.getVisibilityString() 371 val newVisibility = newModifiers.getVisibilityString() 372 if (oldVisibility != newVisibility) { 373 // TODO: Use newModifiers.asAccessibleAs(oldModifiers) to provide different error 374 // messages 375 // based on whether this seems like a reasonable change, e.g. making a private or final 376 // method more 377 // accessible is fine (no overridden method affected) but not making methods less 378 // accessible etc 379 report( 380 Issues.CHANGED_SCOPE, 381 new, 382 "${describe(new, capitalize = true)} changed visibility from $oldVisibility to $newVisibility" 383 ) 384 } 385 386 if (!old.effectivelyDeprecated == new.effectivelyDeprecated) { 387 report( 388 Issues.CHANGED_DEPRECATED, 389 new, 390 "${describe( 391 new, 392 capitalize = true 393 )} has changed deprecation state ${old.effectivelyDeprecated} --> ${new.effectivelyDeprecated}" 394 ) 395 } 396 397 val oldSuperClassName = old.superClass()?.qualifiedName() 398 if (oldSuperClassName != null) { // java.lang.Object can't have a superclass. 399 if (!new.extends(oldSuperClassName)) { 400 report( 401 Issues.CHANGED_SUPERCLASS, 402 new, 403 "${describe( 404 new, 405 capitalize = true 406 )} superclass changed from $oldSuperClassName to ${new.superClass()?.qualifiedName()}" 407 ) 408 } 409 } 410 411 if (old.hasTypeVariables() || new.hasTypeVariables()) { 412 val oldTypeParamsCount = old.typeParameterList.size 413 val newTypeParamsCount = new.typeParameterList.size 414 if (oldTypeParamsCount > 0 && oldTypeParamsCount != newTypeParamsCount) { 415 report( 416 Issues.CHANGED_TYPE, 417 new, 418 "${ 419 describe( 420 old, 421 capitalize = true 422 ) 423 } changed number of type parameters from $oldTypeParamsCount to $newTypeParamsCount" 424 ) 425 } 426 } 427 428 if ( 429 old.modifiers.isAnnotatedWith(JVM_DEFAULT_WITH_COMPATIBILITY) && 430 !new.modifiers.isAnnotatedWith(JVM_DEFAULT_WITH_COMPATIBILITY) 431 ) { 432 report( 433 Issues.REMOVED_JVM_DEFAULT_WITH_COMPATIBILITY, 434 new, 435 "Cannot remove @$JVM_DEFAULT_WITH_COMPATIBILITY annotation from " + 436 "${describe(new)}: Incompatible change" 437 ) 438 } 439 } 440 441 /** 442 * Check if the return types are compatible, which is true when: 443 * - they're equal 444 * - both are arrays, and the component types are compatible 445 * - both are variable types, and they have equal bounds 446 * - the new return type is a variable and has the old return type in its bounds 447 * 448 * TODO(b/111253910): could this also allow changes like List<T> to List<A> where A and T have 449 * equal bounds? 450 */ 451 private fun compatibleReturnTypes(old: TypeItem, new: TypeItem): Boolean { 452 when (new) { 453 is ArrayTypeItem -> 454 return old is ArrayTypeItem && 455 compatibleReturnTypes(old.componentType, new.componentType) 456 is VariableTypeItem -> { 457 if (old is VariableTypeItem) { 458 // If both return types are parameterized then the constraints must be 459 // exactly the same. 460 return old.asTypeParameter.typeBounds() == new.asTypeParameter.typeBounds() 461 } else { 462 // If the old return type was not parameterized but the new return type is, 463 // the new type parameter must have the old return type in its bounds 464 // (e.g. changing return type from `String` to `T extends String` is valid). 465 val constraints = new.asTypeParameter.typeBounds() 466 val oldClass = old.asClass() 467 for (constraint in constraints) { 468 val newClass = constraint.asClass() 469 if ( 470 oldClass == null || 471 newClass == null || 472 !oldClass.extendsOrImplements(newClass.qualifiedName()) 473 ) { 474 return false 475 } 476 } 477 return true 478 } 479 } 480 else -> return old == new 481 } 482 } 483 484 override fun compareCallableItems(old: CallableItem, new: CallableItem) { 485 val oldModifiers = old.modifiers 486 val newModifiers = new.modifiers 487 488 val oldVisibility = oldModifiers.getVisibilityString() 489 val newVisibility = newModifiers.getVisibilityString() 490 if (oldVisibility != newVisibility) { 491 // Only report issue if the change is a decrease in access; e.g. public -> protected 492 if (!newModifiers.asAccessibleAs(oldModifiers)) { 493 report( 494 Issues.CHANGED_SCOPE, 495 new, 496 "${describe(new, capitalize = true)} changed visibility from $oldVisibility to $newVisibility" 497 ) 498 } 499 } 500 501 if (old.effectivelyDeprecated != new.effectivelyDeprecated) { 502 report( 503 Issues.CHANGED_DEPRECATED, 504 new, 505 "${describe( 506 new, 507 capitalize = true 508 )} has changed deprecation state ${old.effectivelyDeprecated} --> ${new.effectivelyDeprecated}" 509 ) 510 } 511 512 for (throwType in old.throwsTypes()) { 513 // Get the throwable class, if none could be found then it is either because there is an 514 // error in the codebase or the codebase is incomplete, either way reporting an error 515 // would be unhelpful. 516 val throwableClass = throwType.erasedClass ?: continue 517 if (!new.throws(throwableClass.qualifiedName())) { 518 // exclude 'throws' changes to finalize() overrides with no arguments 519 if (old.name() != "finalize" || old.parameters().isNotEmpty()) { 520 report( 521 Issues.CHANGED_THROWS, 522 new, 523 "${describe(new, capitalize = true)} no longer throws exception ${throwType.description()}" 524 ) 525 } 526 } 527 } 528 529 for (throwType in new.filteredThrowsTypes(filterReference)) { 530 // Get the throwable class, if none could be found then it is either because there is an 531 // error in the codebase or the codebase is incomplete, either way reporting an error 532 // would be unhelpful. 533 val throwableClass = throwType.erasedClass ?: continue 534 if (!old.throws(throwableClass.qualifiedName())) { 535 // exclude 'throws' changes to finalize() overrides with no arguments 536 if (!(old.name() == "finalize" && old.parameters().isEmpty())) { 537 val message = 538 "${describe(new, capitalize = true)} added thrown exception ${throwType.description()}" 539 report(Issues.CHANGED_THROWS, new, message) 540 } 541 } 542 } 543 } 544 545 override fun compareMethodItems(old: MethodItem, new: MethodItem) { 546 val oldModifiers = old.modifiers 547 val newModifiers = new.modifiers 548 549 val oldReturnType = old.returnType() 550 val newReturnType = new.returnType() 551 552 if (!compatibleReturnTypes(oldReturnType, newReturnType)) { 553 // For incompatible type variable changes, include the type bounds in the string. 554 val oldTypeString = describeBounds(oldReturnType) 555 val newTypeString = describeBounds(newReturnType) 556 val message = 557 "${describe(new, capitalize = true)} has changed return type from $oldTypeString to $newTypeString" 558 report(Issues.CHANGED_TYPE, new, message) 559 } 560 561 // Annotation methods 562 if ( 563 new.containingClass().isAnnotationType() && 564 old.containingClass().isAnnotationType() && 565 new.legacyDefaultValue() != old.legacyDefaultValue() 566 ) { 567 val prevValue = old.legacyDefaultValue() 568 val prevString = 569 if (prevValue.isEmpty()) { 570 "nothing" 571 } else { 572 prevValue 573 } 574 575 val newValue = new.legacyDefaultValue() 576 val newString = 577 if (newValue.isEmpty()) { 578 "nothing" 579 } else { 580 newValue 581 } 582 val message = 583 "${describe( 584 new, 585 capitalize = true 586 )} has changed value from $prevString to $newString" 587 588 // Adding a default value to an annotation method is safe 589 val annotationMethodAddingDefaultValue = 590 new.containingClass().isAnnotationType() && old.legacyDefaultValue().isEmpty() 591 592 if (!annotationMethodAddingDefaultValue) { 593 report(Issues.CHANGED_VALUE, new, message) 594 } 595 } 596 597 // Check for changes in abstract, but only for regular classes; older signature files 598 // sometimes describe interface methods as abstract 599 if (new.containingClass().isClass()) { 600 if (!oldModifiers.isAbstract() && newModifiers.isAbstract()) { 601 report( 602 Issues.CHANGED_ABSTRACT, 603 new, 604 "${describe(new, capitalize = true)} has changed 'abstract' qualifier" 605 ) 606 } 607 } 608 609 if (new.containingClass().isInterface() || new.containingClass().isAnnotationType()) { 610 if (oldModifiers.isDefault() && newModifiers.isAbstract()) { 611 report( 612 Issues.CHANGED_DEFAULT, 613 new, 614 "${describe(new, capitalize = true)} has changed 'default' qualifier" 615 ) 616 } 617 } 618 619 if (oldModifiers.isNative() != newModifiers.isNative()) { 620 report( 621 Issues.CHANGED_NATIVE, 622 new, 623 "${describe(new, capitalize = true)} has changed 'native' qualifier" 624 ) 625 } 626 627 // Check changes to final modifier. But skip enums where it varies between signature files 628 // and PSI 629 // whether the methods are considered final. 630 if (!new.containingClass().isEnum() && !oldModifiers.isStatic()) { 631 // Compiler-generated methods vary in their 'final' qualifier between versions of 632 // the compiler, so this check needs to be quite narrow. A change in 'final' 633 // status of a method is only relevant if (a) the method is not declared 'static' 634 // and (b) the method is not already inferred to be 'final' by virtue of its class. 635 if (!old.isEffectivelyFinal() && new.isEffectivelyFinal()) { 636 if (!old.containingClass().isExtensible()) { 637 report( 638 Issues.ADDED_FINAL_UNINSTANTIABLE, 639 new, 640 "${ 641 describe( 642 new, 643 capitalize = true 644 ) 645 } added 'final' qualifier but containing ${old.containingClass().describe()} was previously uninstantiable and therefore could not be subclassed" 646 ) 647 } else { 648 report( 649 Issues.ADDED_FINAL, 650 new, 651 "${describe(new, capitalize = true)} has added 'final' qualifier" 652 ) 653 } 654 } else if (old.isEffectivelyFinal() && !new.isEffectivelyFinal()) { 655 // Disallowed removing final: If an app inherits the class and starts overriding 656 // the method it's going to crash on earlier versions where the method is final 657 // It doesn't break compatibility in the strict sense, but does make it very 658 // difficult to extend this method in practice. 659 report( 660 Issues.REMOVED_FINAL_STRICT, 661 new, 662 "${describe(new, capitalize = true)} has removed 'final' qualifier" 663 ) 664 } 665 } 666 667 if (oldModifiers.isStatic() != newModifiers.isStatic()) { 668 report( 669 Issues.CHANGED_STATIC, 670 new, 671 "${describe(new, capitalize = true)} has changed 'static' qualifier" 672 ) 673 } 674 675 if (new.modifiers.isInline()) { 676 val oldTypes = old.typeParameterList 677 val newTypes = new.typeParameterList 678 for (i in oldTypes.indices) { 679 if (i == newTypes.size) { 680 break 681 } 682 if (newTypes[i].isReified() && !oldTypes[i].isReified()) { 683 val message = 684 "${ 685 describe( 686 new, 687 capitalize = true 688 ) 689 } made type variable ${newTypes[i].name()} reified: incompatible change" 690 report(Issues.ADDED_REIFIED, new, message) 691 } 692 } 693 } 694 } 695 696 /** 697 * Returns a string representation of the type, including the bounds for a variable type or 698 * array of variable types. 699 * 700 * TODO(b/111253910): combine into [TypeItem.toTypeString] 701 */ 702 private fun describeBounds(type: TypeItem): String { 703 return when (type) { 704 is ArrayTypeItem -> describeBounds(type.componentType) + "[]" 705 is VariableTypeItem -> { 706 type.name + 707 if (type.asTypeParameter.typeBounds().isEmpty()) { 708 " (extends java.lang.Object)" 709 } else { 710 " (extends ${type.asTypeParameter.typeBounds().joinToString(separator = " & ") { it.toTypeString() }})" 711 } 712 } 713 else -> type.toTypeString() 714 } 715 } 716 717 override fun compareFieldItems(old: FieldItem, new: FieldItem) { 718 val oldModifiers = old.modifiers 719 val newModifiers = new.modifiers 720 721 if (!old.isEnumConstant()) { 722 val oldType = old.type() 723 val newType = new.type() 724 if (oldType != newType) { 725 val message = 726 "${describe(new, capitalize = true)} has changed type from $oldType to $newType" 727 report(Issues.CHANGED_TYPE, new, message) 728 } else if (!old.hasSameValue(new)) { 729 val prevValue = old.legacyInitialValue() 730 val prevString = 731 if (prevValue == null && !old.modifiers.isFinal()) { 732 "nothing/not constant" 733 } else { 734 prevValue 735 } 736 737 val newValue = new.legacyInitialValue() 738 val newString = 739 if (newValue is PsiField) { 740 newValue.containingClass?.qualifiedName + "." + newValue.name 741 } else { 742 newValue 743 } 744 val message = 745 "${describe( 746 new, 747 capitalize = true 748 )} has changed value from $prevString to $newString" 749 750 report(Issues.CHANGED_VALUE, new, message) 751 } 752 } 753 754 val oldVisibility = oldModifiers.getVisibilityString() 755 val newVisibility = newModifiers.getVisibilityString() 756 if (oldVisibility != newVisibility) { 757 // Only report issue if the change is a decrease in access; e.g. public -> protected 758 if (!newModifiers.asAccessibleAs(oldModifiers)) { 759 report( 760 Issues.CHANGED_SCOPE, 761 new, 762 "${ 763 describe( 764 new, 765 capitalize = true 766 ) 767 } changed visibility from $oldVisibility to $newVisibility" 768 ) 769 } 770 } 771 772 if (oldModifiers.isStatic() != newModifiers.isStatic()) { 773 report( 774 Issues.CHANGED_STATIC, 775 new, 776 "${describe(new, capitalize = true)} has changed 'static' qualifier" 777 ) 778 } 779 780 if (!oldModifiers.isFinal() && newModifiers.isFinal()) { 781 report( 782 Issues.ADDED_FINAL, 783 new, 784 "${describe(new, capitalize = true)} has added 'final' qualifier" 785 ) 786 } else if ( 787 // Final can't be removed if field is static with compile-time constant 788 oldModifiers.isFinal() && 789 !newModifiers.isFinal() && 790 oldModifiers.isStatic() && 791 old.legacyInitialValue() != null 792 ) { 793 report( 794 Issues.REMOVED_FINAL, 795 new, 796 "${describe(new, capitalize = true)} has removed 'final' qualifier" 797 ) 798 } 799 800 if (oldModifiers.isVolatile() != newModifiers.isVolatile()) { 801 report( 802 Issues.CHANGED_VOLATILE, 803 new, 804 "${describe(new, capitalize = true)} has changed 'volatile' qualifier" 805 ) 806 } 807 808 if (old.effectivelyDeprecated != new.effectivelyDeprecated) { 809 report( 810 Issues.CHANGED_DEPRECATED, 811 new, 812 "${describe( 813 new, 814 capitalize = true 815 )} has changed deprecation state ${old.effectivelyDeprecated} --> ${new.effectivelyDeprecated}" 816 ) 817 } 818 } 819 820 @Suppress("DEPRECATION") 821 private fun handleAdded(issue: Issue, item: SelectableItem) { 822 if (item.originallyHidden) { 823 // This is an element which is hidden but is referenced from 824 // some public API. This is an error, but some existing code 825 // is doing this. This is not an API addition. 826 return 827 } 828 829 if (!filterReference.test(item)) { 830 // This item is something we weren't asked to verify 831 return 832 } 833 834 var message = "Added ${describe(item)}" 835 836 // Clarify error message for removed API to make it less ambiguous 837 if (apiType == ApiType.REMOVED) { 838 message += " to the removed API" 839 } else if (options.allShowAnnotations.isNotEmpty()) { 840 if (options.allShowAnnotations.matchesAnnotationName(ANDROID_SYSTEM_API)) { 841 message += " to the system API" 842 } else if (options.allShowAnnotations.matchesAnnotationName(ANDROID_TEST_API)) { 843 message += " to the test API" 844 } 845 } 846 847 report(issue, item, message) 848 } 849 850 private fun handleRemoved(issue: Issue, item: SelectableItem) { 851 if (!item.emit) { 852 // It's a stub; this can happen when analyzing partial APIs 853 // such as a signature file for a library referencing types 854 // from the upstream library dependencies. 855 return 856 } 857 858 report( 859 issue, 860 item, 861 "Removed ${if (item.effectivelyDeprecated) "deprecated " else ""}${describe(item)}" 862 ) 863 } 864 865 override fun addedPackageItem(new: PackageItem) { 866 handleAdded(Issues.ADDED_PACKAGE, new) 867 } 868 869 override fun addedClassItem(new: ClassItem) { 870 val error = 871 if (new.isInterface()) { 872 Issues.ADDED_INTERFACE 873 } else { 874 Issues.ADDED_CLASS 875 } 876 handleAdded(error, new) 877 } 878 879 override fun addedCallableItem(new: CallableItem) { 880 if (new is MethodItem) { 881 // *Overriding* methods from super classes that are outside the 882 // API is OK (e.g. overriding toString() from java.lang.Object) 883 val superMethods = new.superMethods() 884 for (superMethod in superMethods) { 885 if (superMethod.origin == ClassOrigin.CLASS_PATH) { 886 return 887 } 888 } 889 890 // In most cases it is not permitted to add a new method to an interface, even with a 891 // default implementation because it could create ambiguity if client code implements 892 // two interfaces that each now define methods with the same signature. 893 // Annotation types cannot implement other interfaces, however, so it is permitted to 894 // add new default methods to annotation types. 895 if (new.containingClass().isAnnotationType() && new.legacyDefaultValue() != "") { 896 return 897 } 898 } 899 900 // Do not fail if this "new" method is really an override of an 901 // existing superclass method, but we should fail if this is overriding 902 // an abstract method, because method's abstractness affects how users use it. 903 // See if there's a member from inherited class 904 val inherited = 905 if (new is MethodItem) { 906 new.containingClass() 907 .findMethod(new, includeSuperClasses = true, includeInterfaces = false) 908 } else null 909 910 // It is ok to add a new abstract method to a class that has no public constructors 911 if ( 912 new.containingClass().isClass() && 913 !new.containingClass().constructors().any { it.isPublic && !it.hidden } && 914 new.modifiers.isAbstract() 915 ) { 916 return 917 } 918 919 if (inherited == null || inherited == new || !inherited.modifiers.isAbstract()) { 920 val error = 921 when { 922 new.modifiers.isAbstract() -> Issues.ADDED_ABSTRACT_METHOD 923 new.containingClass().isInterface() -> 924 when { 925 new.modifiers.isStatic() -> Issues.ADDED_METHOD 926 new.modifiers.isDefault() -> { 927 // Hack to always mark added Kotlin interface methods as abstract 928 // until we properly support JVM default methods for Kotlin. 929 // TODO(b/200077254): Remove Kotlin special case 930 if (new.itemLanguage == ItemLanguage.KOTLIN) { 931 Issues.ADDED_ABSTRACT_METHOD 932 } else { 933 Issues.ADDED_METHOD 934 } 935 } 936 else -> Issues.ADDED_ABSTRACT_METHOD 937 } 938 else -> Issues.ADDED_METHOD 939 } 940 handleAdded(error, new) 941 } 942 } 943 944 override fun addedFieldItem(new: FieldItem) { 945 handleAdded(Issues.ADDED_FIELD, new) 946 } 947 948 override fun removedPackageItem(old: PackageItem, from: PackageItem?) { 949 handleRemoved(Issues.REMOVED_PACKAGE, old) 950 } 951 952 override fun removedClassItem(old: ClassItem, from: SelectableItem) { 953 val error = 954 when { 955 old.isInterface() -> Issues.REMOVED_INTERFACE 956 old.effectivelyDeprecated -> Issues.REMOVED_DEPRECATED_CLASS 957 else -> Issues.REMOVED_CLASS 958 } 959 960 handleRemoved(error, old) 961 } 962 963 override fun removedCallableItem(old: CallableItem, from: ClassItem) { 964 // See if there's a member from inherited class 965 val inherited = 966 if (old is MethodItem) { 967 // This can also return self, specially handled below 968 from 969 .findMethod( 970 old, 971 includeSuperClasses = true, 972 includeInterfaces = from.isInterface() 973 ) 974 ?.let { 975 // If it was inherited but should still be treated as if it was removed then 976 // pretend that it was not inherited. 977 if (it.treatAsRemoved(old)) null else it 978 } 979 } else null 980 981 if (inherited == null) { 982 val error = 983 if (old.effectivelyDeprecated) Issues.REMOVED_DEPRECATED_METHOD 984 else Issues.REMOVED_METHOD 985 handleRemoved(error, old) 986 } 987 } 988 989 /** 990 * Check the [Item] to see whether it should be treated as if it was removed. 991 * 992 * If an [Item] is an unstable API that will be reverted then it will not be treated as if it 993 * was removed. That is because reverting it will replace it with the old item against which it 994 * is being compared in this compatibility check. So, while this specific item will not appear 995 * in the API the old item will and so it has not been removed. 996 * 997 * Otherwise, an [Item] will be treated as it was removed it if it is hidden/removed or the 998 * [possibleMatch] does not match. 999 */ 1000 private fun MethodItem.treatAsRemoved(possibleMatch: MethodItem) = 1001 !showability.revertUnstableApi() && (isHiddenOrRemoved() || this != possibleMatch) 1002 1003 override fun removedFieldItem(old: FieldItem, from: ClassItem) { 1004 val inherited = 1005 from.findField( 1006 old.name(), 1007 includeSuperClasses = true, 1008 includeInterfaces = from.isInterface() 1009 ) 1010 if (inherited == null) { 1011 val error = 1012 if (old.effectivelyDeprecated) Issues.REMOVED_DEPRECATED_FIELD 1013 else Issues.REMOVED_FIELD 1014 handleRemoved(error, old) 1015 } 1016 } 1017 1018 private fun report( 1019 issue: Issue, 1020 item: Item, 1021 message: String, 1022 location: FileLocation = FileLocation.UNKNOWN, 1023 maximumSeverity: Severity = Severity.UNLIMITED, 1024 ) { 1025 if (item.isCompatibilitySuppressed()) { 1026 // Long-term, we should consider allowing meta-annotations to specify a different 1027 // `configuration` so it can use a separate set of severities. For now, though, we'll 1028 // treat all issues for all unchecked items as `Severity.IGNORE`. 1029 return 1030 } 1031 if (reporter.report(issue, item, message, location, maximumSeverity = maximumSeverity)) { 1032 // If the issue was reported and was an error then remember that this found some 1033 // problems so that the process can be aborted after finishing the checks. 1034 val severity = minOf(maximumSeverity, issueConfiguration.getSeverity(issue)) 1035 if (severity == Severity.ERROR) { 1036 foundProblems = true 1037 } 1038 } 1039 } 1040 1041 companion object { 1042 @Suppress("DEPRECATION") 1043 fun checkCompatibility( 1044 newCodebase: Codebase, 1045 oldCodebase: Codebase, 1046 apiType: ApiType, 1047 reporter: Reporter, 1048 issueConfiguration: IssueConfiguration, 1049 apiCompatAnnotations: Set<String>, 1050 ) { 1051 val filter = 1052 apiType 1053 .getReferenceFilter(options.apiPredicateConfig) 1054 .or(apiType.getEmitFilter(options.apiPredicateConfig)) 1055 .or(ApiType.PUBLIC_API.getReferenceFilter(options.apiPredicateConfig)) 1056 .or(ApiType.PUBLIC_API.getEmitFilter(options.apiPredicateConfig)) 1057 1058 val checker = 1059 CompatibilityCheck( 1060 filter, 1061 apiType, 1062 reporter, 1063 issueConfiguration, 1064 apiCompatAnnotations, 1065 ) 1066 1067 val oldFullCodebase = 1068 if (options.showUnannotated && apiType == ApiType.PUBLIC_API) { 1069 MergedCodebase(listOf(oldCodebase)) 1070 } else { 1071 // To avoid issues with partial oldCodeBase we fill gaps with newCodebase, the 1072 // first parameter is master, so we don't change values of oldCodeBase 1073 MergedCodebase(listOf(oldCodebase, newCodebase)) 1074 } 1075 val newFullCodebase = MergedCodebase(listOf(newCodebase)) 1076 1077 CodebaseComparator().compare(checker, oldFullCodebase, newFullCodebase, filter) 1078 1079 val message = 1080 "Found compatibility problems checking " + 1081 "the ${apiType.displayName} API (${newCodebase.location}) against the API in ${oldCodebase.location}" 1082 1083 if (checker.foundProblems) { 1084 cliError(message) 1085 } 1086 } 1087 } 1088 } 1089