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