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