• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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