• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
18 
19 import com.android.tools.metalava.NullnessMigration.Companion.findNullnessAnnotation
20 import com.android.tools.metalava.NullnessMigration.Companion.isNullable
21 import com.android.tools.metalava.doclava1.ApiPredicate
22 import com.android.tools.metalava.doclava1.Issues
23 import com.android.tools.metalava.doclava1.Issues.Issue
24 import com.android.tools.metalava.doclava1.TextCodebase
25 import com.android.tools.metalava.model.AnnotationItem
26 import com.android.tools.metalava.model.ClassItem
27 import com.android.tools.metalava.model.Codebase
28 import com.android.tools.metalava.model.FieldItem
29 import com.android.tools.metalava.model.Item
30 import com.android.tools.metalava.model.Item.Companion.describe
31 import com.android.tools.metalava.model.MethodItem
32 import com.android.tools.metalava.model.PackageItem
33 import com.android.tools.metalava.model.ParameterItem
34 import com.android.tools.metalava.model.TypeItem
35 import com.android.tools.metalava.model.configuration
36 import com.intellij.psi.PsiField
37 import java.io.File
38 import java.util.function.Predicate
39 
40 /**
41  * Compares the current API with a previous version and makes sure
42  * the changes are compatible. For example, you can make a previously
43  * nullable parameter non null, but not vice versa.
44  *
45  * TODO: Only allow nullness changes on final classes!
46  */
47 class CompatibilityCheck(
48     val filterReference: Predicate<Item>,
49     private val oldCodebase: Codebase,
50     private val apiType: ApiType,
51     private val base: Codebase? = null,
52     private val reporter: Reporter
53 ) : ComparisonVisitor() {
54 
55     /**
56      * Request for compatibility checks.
57      * [file] represents the signature file to be checked. [apiType] represents which
58      * part of the API should be checked, [releaseType] represents what kind of codebase
59      * we are comparing it against. If [codebase] is specified, compare the signature file
60      * against the codebase instead of metalava's current source tree configured via the
61      * normal source path flags.
62      */
63     data class CheckRequest(
64         val file: File,
65         val apiType: ApiType,
66         val releaseType: ReleaseType,
67         val codebase: File? = null
68     ) {
toStringnull69         override fun toString(): String {
70             return "--check-compatibility:${apiType.flagName}:${releaseType.flagName} $file"
71         }
72     }
73 
74     /** In old signature files, methods inherited from hidden super classes
75      * are not included. An example of this is StringBuilder.setLength.
76      * More details about this are listed in Compatibility.skipInheritedMethods.
77      * We may see these in the codebase but not in the (old) signature files,
78      * so in these cases we want to ignore certain changes such as considering
79      * StringBuilder.setLength a newly added method.
80      */
81     private val comparingWithPartialSignatures = oldCodebase is TextCodebase && oldCodebase.format == FileFormat.V1
82 
83     var foundProblems = false
84 
comparenull85     override fun compare(old: Item, new: Item) {
86         val oldModifiers = old.modifiers
87         val newModifiers = new.modifiers
88         if (oldModifiers.isOperator() && !newModifiers.isOperator()) {
89             report(
90                 Issues.OPERATOR_REMOVAL,
91                 new,
92                 "Cannot remove `operator` modifier from ${describe(new)}: Incompatible change"
93             )
94         }
95 
96         if (oldModifiers.isInfix() && !newModifiers.isInfix()) {
97             report(
98                 Issues.INFIX_REMOVAL,
99                 new,
100                 "Cannot remove `infix` modifier from ${describe(new)}: Incompatible change"
101             )
102         }
103 
104         // Should not remove nullness information
105         // Can't change information incompatibly
106         val oldNullnessAnnotation = findNullnessAnnotation(old)
107         if (oldNullnessAnnotation != null) {
108             val newNullnessAnnotation = findNullnessAnnotation(new)
109             if (newNullnessAnnotation == null) {
110                 val implicitNullness = AnnotationItem.getImplicitNullness(new)
111                 if (implicitNullness == true && isNullable(old)) {
112                     return
113                 }
114                 if (implicitNullness == false && !isNullable(old)) {
115                     return
116                 }
117                 val name = AnnotationItem.simpleName(oldNullnessAnnotation)
118                 if (old.type()?.primitive == true) {
119                     return
120                 }
121                 report(
122                     Issues.INVALID_NULL_CONVERSION, new,
123                     "Attempted to remove $name annotation from ${describe(new)}"
124                 )
125             } else {
126                 val oldNullable = isNullable(old)
127                 val newNullable = isNullable(new)
128                 if (oldNullable != newNullable) {
129                     // You can change a parameter from nonnull to nullable
130                     // You can change a method from nullable to nonnull
131                     // You cannot change a parameter from nullable to nonnull
132                     // You cannot change a method from nonnull to nullable
133                     if (oldNullable && old is ParameterItem) {
134                         report(
135                             Issues.INVALID_NULL_CONVERSION,
136                             new,
137                             "Attempted to change parameter from @Nullable to @NonNull: " +
138                                 "incompatible change for ${describe(new)}"
139                         )
140                     } else if (!oldNullable && old is MethodItem) {
141                         report(
142                             Issues.INVALID_NULL_CONVERSION,
143                             new,
144                             "Attempted to change method return from @NonNull to @Nullable: " +
145                                 "incompatible change for ${describe(new)}"
146                         )
147                     }
148                 }
149             }
150         }
151     }
152 
comparenull153     override fun compare(old: ParameterItem, new: ParameterItem) {
154         val prevName = old.publicName() ?: return
155         val newName = new.publicName()
156         if (newName == null) {
157             report(
158                 Issues.PARAMETER_NAME_CHANGE,
159                 new,
160                 "Attempted to remove parameter name from ${describe(new)} in ${describe(new.containingMethod())}"
161             )
162         } else if (newName != prevName) {
163             report(
164                 Issues.PARAMETER_NAME_CHANGE,
165                 new,
166                 "Attempted to change parameter name from $prevName to $newName in ${describe(new.containingMethod())}"
167             )
168         }
169 
170         if (old.hasDefaultValue() && !new.hasDefaultValue()) {
171             report(
172                 Issues.DEFAULT_VALUE_CHANGE,
173                 new,
174                 "Attempted to remove default value from ${describe(new)} in ${describe(new.containingMethod())}"
175             )
176         }
177 
178         if (old.isVarArgs() && !new.isVarArgs()) {
179             // In Java, changing from array to varargs is a compatible change, but
180             // not the other way around. Kotlin is the same, though in Kotlin
181             // you have to change the parameter type as well to an array type; assuming you
182             // do that it's the same situation as Java; otherwise the normal
183             // signature check will catch the incompatibility.
184             report(
185                 Issues.VARARG_REMOVAL,
186                 new,
187                 "Changing from varargs to array is an incompatible change: ${describe(
188                     new,
189                     includeParameterTypes = true,
190                     includeParameterNames = true
191                 )}"
192             )
193         }
194     }
195 
comparenull196     override fun compare(old: ClassItem, new: ClassItem) {
197         val oldModifiers = old.modifiers
198         val newModifiers = new.modifiers
199 
200         if (old.isInterface() != new.isInterface()) {
201             report(
202                 Issues.CHANGED_CLASS, new, "${describe(new, capitalize = true)} changed class/interface declaration"
203             )
204             return // Avoid further warnings like "has changed abstract qualifier" which is implicit in this change
205         }
206 
207         for (iface in old.interfaceTypes()) {
208             val qualifiedName = iface.asClass()?.qualifiedName() ?: continue
209             if (!new.implements(qualifiedName)) {
210                 report(
211                     Issues.REMOVED_INTERFACE, new, "${describe(old, capitalize = true)} no longer implements $iface"
212                 )
213             }
214         }
215 
216         for (iface in new.filteredInterfaceTypes(filterReference)) {
217             val qualifiedName = iface.asClass()?.qualifiedName() ?: continue
218             if (!old.implements(qualifiedName)) {
219                 report(
220                     Issues.ADDED_INTERFACE, new, "Added interface $iface to class ${describe(old)}"
221                 )
222             }
223         }
224 
225         if (!oldModifiers.isSealed() && newModifiers.isSealed()) {
226             report(Issues.ADD_SEALED, new, "Cannot add 'sealed' modifier to ${describe(new)}: Incompatible change")
227         } else if (old.isClass() && oldModifiers.isAbstract() != newModifiers.isAbstract()) {
228             report(
229                 Issues.CHANGED_ABSTRACT, new, "${describe(new, capitalize = true)} changed 'abstract' qualifier"
230             )
231         }
232 
233         // Check for changes in final & static, but not in enums (since PSI and signature files differ
234         // a bit in whether they include these for enums
235         if (!new.isEnum()) {
236             if (!oldModifiers.isFinal() && newModifiers.isFinal()) {
237                 // It is safe to make a class final if it did not previously have any public
238                 // constructors because it was impossible for an application to create a subclass.
239                 if (old.constructors().filter { it.isPublic || it.isProtected }.none()) {
240                     report(
241                         Issues.ADDED_FINAL_UNINSTANTIABLE, new,
242                         "${describe(
243                             new,
244                             capitalize = true
245                         )} added 'final' qualifier but was previously uninstantiable and therefore could not be subclassed"
246                     )
247                 } else {
248                     report(
249                         Issues.ADDED_FINAL, new, "${describe(new, capitalize = true)} added 'final' qualifier"
250                     )
251                 }
252             } else if (oldModifiers.isFinal() && !newModifiers.isFinal()) {
253                 report(
254                     Issues.REMOVED_FINAL, new, "${describe(new, capitalize = true)} removed 'final' qualifier"
255                 )
256             }
257 
258             if (oldModifiers.isStatic() != newModifiers.isStatic()) {
259                 val hasPublicConstructor = old.constructors().any { it.isPublic }
260                 if (!old.isInnerClass() || hasPublicConstructor) {
261                     report(
262                         Issues.CHANGED_STATIC,
263                         new,
264                         "${describe(new, capitalize = true)} changed 'static' qualifier"
265                     )
266                 }
267             }
268         }
269 
270         val oldVisibility = oldModifiers.getVisibilityString()
271         val newVisibility = newModifiers.getVisibilityString()
272         if (oldVisibility != newVisibility) {
273             // TODO: Use newModifiers.asAccessibleAs(oldModifiers) to provide different error messages
274             // based on whether this seems like a reasonable change, e.g. making a private or final method more
275             // accessible is fine (no overridden method affected) but not making methods less accessible etc
276             report(
277                 Issues.CHANGED_SCOPE, new,
278                 "${describe(new, capitalize = true)} changed visibility from $oldVisibility to $newVisibility"
279             )
280         }
281 
282         if (!old.deprecated == new.deprecated) {
283             report(
284                 Issues.CHANGED_DEPRECATED, new,
285                 "${describe(
286                     new,
287                     capitalize = true
288                 )} has changed deprecation state ${old.deprecated} --> ${new.deprecated}"
289             )
290         }
291 
292         val oldSuperClassName = old.superClass()?.qualifiedName()
293         if (oldSuperClassName != null) { // java.lang.Object can't have a superclass.
294             if (!new.extends(oldSuperClassName)) {
295                 report(
296                     Issues.CHANGED_SUPERCLASS, new,
297                     "${describe(
298                         new,
299                         capitalize = true
300                     )} superclass changed from $oldSuperClassName to ${new.superClass()?.qualifiedName()}"
301                 )
302             }
303         }
304 
305         if (old.hasTypeVariables() && new.hasTypeVariables()) {
306             val oldTypeParamsCount = old.typeParameterList().typeParameterCount()
307             val newTypeParamsCount = new.typeParameterList().typeParameterCount()
308             if (oldTypeParamsCount != newTypeParamsCount) {
309                 report(
310                     Issues.CHANGED_TYPE, new,
311                     "${describe(
312                         old,
313                         capitalize = true
314                     )} changed number of type parameters from $oldTypeParamsCount to $newTypeParamsCount"
315                 )
316             }
317         }
318     }
319 
comparenull320     override fun compare(old: MethodItem, new: MethodItem) {
321         val oldModifiers = old.modifiers
322         val newModifiers = new.modifiers
323 
324         val oldReturnType = old.returnType()
325         val newReturnType = new.returnType()
326         if (!new.isConstructor() && oldReturnType != null && newReturnType != null) {
327             val oldTypeParameter = oldReturnType.asTypeParameter(old)
328             val newTypeParameter = newReturnType.asTypeParameter(new)
329             var compatible = true
330             if (oldTypeParameter == null &&
331                 newTypeParameter == null
332             ) {
333                 if (oldReturnType != newReturnType ||
334                     oldReturnType.arrayDimensions() != newReturnType.arrayDimensions()
335                 ) {
336                     compatible = false
337                 }
338             } else if (oldTypeParameter == null && newTypeParameter != null) {
339                 val constraints = newTypeParameter.bounds()
340                 for (constraint in constraints) {
341                     val oldClass = oldReturnType.asClass()
342                     if (oldClass == null || !oldClass.extendsOrImplements(constraint.qualifiedName())) {
343                         compatible = false
344                     }
345                 }
346             } else if (oldTypeParameter != null && newTypeParameter == null) {
347                 // It's never valid to go from being a parameterized type to not being one.
348                 // This would drop the implicit cast breaking backwards compatibility.
349                 compatible = false
350             } else {
351                 // If both return types are parameterized then the constraints must be
352                 // exactly the same.
353                 val oldConstraints = oldTypeParameter?.bounds() ?: emptyList()
354                 val newConstraints = newTypeParameter?.bounds() ?: emptyList()
355                 if (oldConstraints.size != newConstraints.size ||
356                     newConstraints != oldConstraints
357                 ) {
358                     val oldTypeString = describeBounds(oldReturnType, oldConstraints)
359                     val newTypeString = describeBounds(newReturnType, newConstraints)
360                     val message =
361                         "${describe(
362                             new,
363                             capitalize = true
364                         )} has changed return type from $oldTypeString to $newTypeString"
365 
366                     report(Issues.CHANGED_TYPE, new, message)
367                     return
368                 }
369             }
370 
371             if (!compatible) {
372                 var oldTypeString = oldReturnType.toSimpleType()
373                 var newTypeString = newReturnType.toSimpleType()
374                 // Typically, show short type names like "String" if they're distinct (instead of long type names like
375                 // "java.util.Set<T!>")
376                 if (oldTypeString == newTypeString) {
377                     // If the short names aren't unique, then show full type names like "java.util.Set<T!>"
378                     oldTypeString = oldReturnType.toString()
379                     newTypeString = newReturnType.toString()
380                 }
381                 val message =
382                     "${describe(new, capitalize = true)} has changed return type from $oldTypeString to $newTypeString"
383                 report(Issues.CHANGED_TYPE, new, message)
384             }
385 
386             // Annotation methods?
387             if (!old.hasSameValue(new)) {
388                 val prevValue = old.defaultValue()
389                 val prevString = if (prevValue.isEmpty()) {
390                     "nothing"
391                 } else {
392                     prevValue
393                 }
394 
395                 val newValue = new.defaultValue()
396                 val newString = if (newValue.isEmpty()) {
397                     "nothing"
398                 } else {
399                     newValue
400                 }
401                 val message = "${describe(
402                     new,
403                     capitalize = true
404                 )} has changed value from $prevString to $newString"
405                 report(Issues.CHANGED_VALUE, new, message)
406             }
407         }
408 
409         // Check for changes in abstract, but only for regular classes; older signature files
410         // sometimes describe interface methods as abstract
411         if (new.containingClass().isClass()) {
412             if (!oldModifiers.isAbstract() && newModifiers.isAbstract() &&
413                 // In old signature files, overridden methods of abstract methods declared
414                 // in super classes are sometimes omitted by doclava. This means that the method
415                 // looks (from the signature file perspective) like it has not been implemented,
416                 // whereas in reality it has. For just one example of this, consider
417                 // FragmentBreadCrumbs.onLayout: it's a concrete implementation in that class
418                 // of the inherited method from ViewGroup. However, in the signature file,
419                 // FragmentBreadCrumbs does not list this method; it's only listed (as abstract)
420                 // in the super class. In this scenario, the compatibility check would believe
421                 // the old method in FragmentBreadCrumbs is abstract and the new method is not,
422                 // which is not the case. Therefore, if the old method is coming from a signature
423                 // file based codebase with an old format, we omit abstract change warnings.
424                 // The reverse situation can also happen: AbstractSequentialList defines listIterator
425                 // as abstract, but it's not recorded as abstract in the signature files anywhere,
426                 // so we treat this as a nearly abstract method, which it is not.
427                 (old.inheritedFrom == null || !comparingWithPartialSignatures)
428             ) {
429                 report(
430                     Issues.CHANGED_ABSTRACT, new, "${describe(new, capitalize = true)} has changed 'abstract' qualifier"
431                 )
432             }
433         }
434 
435         if (oldModifiers.isNative() != newModifiers.isNative()) {
436             report(
437                 Issues.CHANGED_NATIVE, new, "${describe(new, capitalize = true)} has changed 'native' qualifier"
438             )
439         }
440 
441         // Check changes to final modifier. But skip enums where it varies between signature files and PSI
442         // whether the methods are considered final.
443         if (!new.containingClass().isEnum() && !oldModifiers.isStatic()) {
444             // Skip changes in final; modifier change could come from inherited
445             // implementation from hidden super class. An example of this
446             // is SpannableString.charAt whose implementation comes from
447             // SpannableStringInternal.
448             if (old.inheritedFrom == null || !comparingWithPartialSignatures) {
449                 // Compiler-generated methods vary in their 'final' qualifier between versions of
450                 // the compiler, so this check needs to be quite narrow. A change in 'final'
451                 // status of a method is only relevant if (a) the method is not declared 'static'
452                 // and (b) the method is not already inferred to be 'final' by virtue of its class.
453                 if (!old.isEffectivelyFinal() && new.isEffectivelyFinal()) {
454                     report(
455                         Issues.ADDED_FINAL, new, "${describe(new, capitalize = true)} has added 'final' qualifier"
456                     )
457                 } else if (old.isEffectivelyFinal() && !new.isEffectivelyFinal()) {
458                     report(
459                         Issues.REMOVED_FINAL, new, "${describe(new, capitalize = true)} has removed 'final' qualifier"
460                     )
461                 }
462             }
463         }
464 
465         if (oldModifiers.isStatic() != newModifiers.isStatic()) {
466             report(
467                 Issues.CHANGED_STATIC, new, "${describe(new, capitalize = true)} has changed 'static' qualifier"
468             )
469         }
470 
471         val oldVisibility = oldModifiers.getVisibilityString()
472         val newVisibility = newModifiers.getVisibilityString()
473         if (oldVisibility != newVisibility) {
474             // TODO: Use newModifiers.asAccessibleAs(oldModifiers) to provide different error messages
475             // based on whether this seems like a reasonable change, e.g. making a private or final method more
476             // accessible is fine (no overridden method affected) but not making methods less accessible etc
477             report(
478                 Issues.CHANGED_SCOPE, new,
479                 "${describe(new, capitalize = true)} changed visibility from $oldVisibility to $newVisibility"
480             )
481         }
482 
483         if (old.deprecated != new.deprecated) {
484             report(
485                 Issues.CHANGED_DEPRECATED, new,
486                 "${describe(
487                     new,
488                     capitalize = true
489                 )} has changed deprecation state ${old.deprecated} --> ${new.deprecated}"
490             )
491         }
492 
493         /*
494         // see JLS 3 13.4.20 "Adding or deleting a synchronized modifier of a method does not break "
495         // "compatibility with existing binaries."
496         if (oldModifiers.isSynchronized() != newModifiers.isSynchronized()) {
497             report(
498                 Errors.CHANGED_SYNCHRONIZED, new,
499                 "${describe(
500                     new,
501                     capitalize = true
502                 )} has changed 'synchronized' qualifier from ${oldModifiers.isSynchronized()} to ${newModifiers.isSynchronized()}"
503             )
504         }
505         */
506 
507         for (exception in old.throwsTypes()) {
508             if (!new.throws(exception.qualifiedName())) {
509                 // exclude 'throws' changes to finalize() overrides with no arguments
510                 if (old.name() != "finalize" || old.parameters().isNotEmpty()) {
511                     report(
512                         Issues.CHANGED_THROWS, new,
513                         "${describe(new, capitalize = true)} no longer throws exception ${exception.qualifiedName()}"
514                     )
515                 }
516             }
517         }
518 
519         for (exec in new.filteredThrowsTypes(filterReference)) {
520             // exclude 'throws' changes to finalize() overrides with no arguments
521             if (!old.throws(exec.qualifiedName())) {
522                 if (old.name() != "finalize" || old.parameters().isNotEmpty()) {
523                     val message = "${describe(new, capitalize = true)} added thrown exception ${exec.qualifiedName()}"
524                     report(Issues.CHANGED_THROWS, new, message)
525                 }
526             }
527         }
528 
529         if (new.modifiers.isInline()) {
530             val oldTypes = old.typeParameterList().typeParameters()
531             val newTypes = new.typeParameterList().typeParameters()
532             for (i in 0 until oldTypes.size) {
533                 if (i == newTypes.size) {
534                     break
535                 }
536                 if (newTypes[i].isReified() && !oldTypes[i].isReified()) {
537                     val message = "${describe(
538                         new,
539                         capitalize = true
540                     )} made type variable ${newTypes[i].simpleName()} reified: incompatible change"
541                     report(Issues.CHANGED_THROWS, new, message)
542                 }
543             }
544         }
545     }
546 
describeBoundsnull547     private fun describeBounds(
548         type: TypeItem,
549         constraints: List<ClassItem>
550     ): String {
551         return type.toSimpleType() +
552             if (constraints.isEmpty()) {
553                 " (extends java.lang.Object)"
554             } else {
555                 " (extends ${constraints.joinToString(separator = " & ") { it.qualifiedName() }})"
556             }
557     }
558 
comparenull559     override fun compare(old: FieldItem, new: FieldItem) {
560         val oldModifiers = old.modifiers
561         val newModifiers = new.modifiers
562 
563         if (!old.isEnumConstant()) {
564             val oldType = old.type()
565             val newType = new.type()
566             if (oldType != newType) {
567                 val message = "${describe(new, capitalize = true)} has changed type from $oldType to $newType"
568                 report(Issues.CHANGED_TYPE, new, message)
569             } else if (!old.hasSameValue(new)) {
570                 val prevValue = old.initialValue(true)
571                 val prevString = if (prevValue == null && !old.modifiers.isFinal()) {
572                     "nothing/not constant"
573                 } else {
574                     prevValue
575                 }
576 
577                 val newValue = new.initialValue(true)
578                 val newString = if (newValue is PsiField) {
579                     newValue.containingClass?.qualifiedName + "." + newValue.name
580                 } else {
581                     newValue
582                 }
583                 val message = "${describe(
584                     new,
585                     capitalize = true
586                 )} has changed value from $prevString to $newString"
587 
588                 if (message == "Field android.telephony.data.ApnSetting.TYPE_DEFAULT has changed value from 17 to 1") {
589                     // Temporarily ignore: this value changed incompatibly from 28.txt to current.txt.
590                     // It's not clear yet whether this value change needs to be reverted, or suppressed
591                     // permanently in the source code, but suppressing from metalava so we can unblock
592                     // getting the compatibility checks enabled.
593                 } else
594                     report(Issues.CHANGED_VALUE, new, message)
595             }
596         }
597 
598         val oldVisibility = oldModifiers.getVisibilityString()
599         val newVisibility = newModifiers.getVisibilityString()
600         if (oldVisibility != newVisibility) {
601             // TODO: Use newModifiers.asAccessibleAs(oldModifiers) to provide different error messages
602             // based on whether this seems like a reasonable change, e.g. making a private or final method more
603             // accessible is fine (no overridden method affected) but not making methods less accessible etc
604             report(
605                 Issues.CHANGED_SCOPE, new,
606                 "${describe(new, capitalize = true)} changed visibility from $oldVisibility to $newVisibility"
607             )
608         }
609 
610         if (oldModifiers.isStatic() != newModifiers.isStatic()) {
611             report(
612                 Issues.CHANGED_STATIC, new, "${describe(new, capitalize = true)} has changed 'static' qualifier"
613             )
614         }
615 
616         if (!oldModifiers.isFinal() && newModifiers.isFinal()) {
617             report(
618                 Issues.ADDED_FINAL, new, "${describe(new, capitalize = true)} has added 'final' qualifier"
619             )
620         } else if (oldModifiers.isFinal() && !newModifiers.isFinal()) {
621             report(
622                 Issues.REMOVED_FINAL, new, "${describe(new, capitalize = true)} has removed 'final' qualifier"
623             )
624         }
625 
626         if (oldModifiers.isTransient() != newModifiers.isTransient()) {
627             report(
628                 Issues.CHANGED_TRANSIENT, new, "${describe(new, capitalize = true)} has changed 'transient' qualifier"
629             )
630         }
631 
632         if (oldModifiers.isVolatile() != newModifiers.isVolatile()) {
633             report(
634                 Issues.CHANGED_VOLATILE, new, "${describe(new, capitalize = true)} has changed 'volatile' qualifier"
635             )
636         }
637 
638         if (old.deprecated != new.deprecated) {
639             report(
640                 Issues.CHANGED_DEPRECATED, new,
641                 "${describe(
642                     new,
643                     capitalize = true
644                 )} has changed deprecation state ${old.deprecated} --> ${new.deprecated}"
645             )
646         }
647     }
648 
handleAddednull649     private fun handleAdded(issue: Issue, item: Item) {
650         if (item.originallyHidden) {
651             // This is an element which is hidden but is referenced from
652             // some public API. This is an error, but some existing code
653             // is doing this. This is not an API addition.
654             return
655         }
656 
657         var message = "Added ${describe(item)}"
658 
659         // Clarify error message for removed API to make it less ambiguous
660         if (apiType == ApiType.REMOVED) {
661             message += " to the removed API"
662         } else if (options.showAnnotations.isNotEmpty()) {
663             if (options.showAnnotations.matchesSuffix("SystemApi")) {
664                 message += " to the system API"
665             } else if (options.showAnnotations.matchesSuffix("TestApi")) {
666                 message += " to the test API"
667             }
668         }
669 
670         // In some cases we run the comparison on signature files
671         // generated into the temp directory, but in these cases
672         // try to report the item against the real item in the API instead
673         val equivalent = findBaseItem(item)
674         if (equivalent != null) {
675             report(issue, equivalent, message)
676             return
677         }
678 
679         report(issue, item, message)
680     }
681 
handleRemovednull682     private fun handleRemoved(issue: Issue, item: Item) {
683         if (!item.emit) {
684             // It's a stub; this can happen when analyzing partial APIs
685             // such as a signature file for a library referencing types
686             // from the upstream library dependencies.
687             return
688         }
689 
690         if (base != null) {
691             // We're diffing "overlay" APIs, such as system or test API files,
692             // where the signature files only list a delta from the full, "base" API.
693             // In that case, if an API is promoted from @SystemApi or @TestApi to be
694             // a full part of the API, it will look like a removal; it appeared in the
695             // previous file and not in the new file, but it's not removed, it's just
696             // not a delta anymore.
697             //
698             // For that reason, we also pass in the "base" API in these cases, and when
699             // an item is removed, we also check the full API to see if it's present
700             // there, and if so, this item is not actually deleted.
701             val baseItem = findBaseItem(item)
702             if (baseItem != null && ApiPredicate(ignoreShown = true).test(baseItem)) {
703                 return
704             }
705         }
706 
707         report(issue, item, "Removed ${if (item.deprecated) "deprecated " else ""}${describe(item)}")
708     }
709 
findBaseItemnull710     private fun findBaseItem(
711         item: Item
712     ): Item? {
713         base ?: return null
714 
715         return when (item) {
716             is PackageItem -> base.findPackage(item.qualifiedName())
717             is ClassItem -> base.findClass(item.qualifiedName())
718             is MethodItem -> base.findClass(item.containingClass().qualifiedName())?.findMethod(
719                 item,
720                 true,
721                 true
722             )
723             is FieldItem -> base.findClass(item.containingClass().qualifiedName())?.findField(item.name())
724             else -> null
725         }
726     }
727 
addednull728     override fun added(new: PackageItem) {
729         handleAdded(Issues.ADDED_PACKAGE, new)
730     }
731 
addednull732     override fun added(new: ClassItem) {
733         val error = if (new.isInterface()) {
734             Issues.ADDED_INTERFACE
735         } else {
736             Issues.ADDED_CLASS
737         }
738         handleAdded(error, new)
739     }
740 
addednull741     override fun added(new: MethodItem) {
742         // In old signature files, methods inherited from hidden super classes
743         // are not included. An example of this is StringBuilder.setLength.
744         // More details about this are listed in Compatibility.skipInheritedMethods.
745         // We may see these in the codebase but not in the (old) signature files,
746         // so skip these -- they're not really "added".
747         if (new.inheritedFrom != null && comparingWithPartialSignatures) {
748             return
749         }
750 
751         // *Overriding* methods from super classes that are outside the
752         // API is OK (e.g. overriding toString() from java.lang.Object)
753         val superMethods = new.superMethods()
754         for (superMethod in superMethods) {
755             if (superMethod.isFromClassPath()) {
756                 return
757             }
758         }
759 
760         // Do not fail if this "new" method is really an override of an
761         // existing superclass method, but we should fail if this is overriding
762         // an abstract method, because method's abstractness affects how users use it.
763         // See if there's a member from inherited class
764         val inherited = if (new.isConstructor()) {
765             null
766         } else {
767             new.containingClass().findMethod(
768                 new,
769                 includeSuperClasses = true,
770                 includeInterfaces = false
771             )
772         }
773 
774         // Builtin annotation methods: just a difference in signature file
775         if ((new.name() == "values" && new.parameters().isEmpty() || new.name() == "valueOf" &&
776                 new.parameters().size == 1) && new.containingClass().isEnum()
777         ) {
778             return
779         }
780 
781         // In old signature files, annotation methods are missing! This will show up as an added method.
782         if (new.containingClass().isAnnotationType() && oldCodebase is TextCodebase && oldCodebase.format == FileFormat.V1) {
783             return
784         }
785 
786         // In most cases it is not permitted to add a new method to an interface, even with a
787         // default implementation because it could could create ambiguity if client code implements
788         // two interfaces that each now define methods with the same signature.
789         // Annotation types cannot implement other interfaces, however, so it is permitted to add
790         // add new default methods to annotation types.
791         if (new.containingClass().isAnnotationType() && new.hasDefaultValue()) {
792             return
793         }
794 
795         if (inherited == null || inherited == new || !inherited.modifiers.isAbstract()) {
796             val error = if (new.modifiers.isAbstract()) Issues.ADDED_ABSTRACT_METHOD else Issues.ADDED_METHOD
797             handleAdded(error, new)
798         }
799     }
800 
addednull801     override fun added(new: FieldItem) {
802         if (new.inheritedFrom != null && comparingWithPartialSignatures) {
803             return
804         }
805 
806         handleAdded(Issues.ADDED_FIELD, new)
807     }
808 
removednull809     override fun removed(old: PackageItem, from: Item?) {
810         handleRemoved(Issues.REMOVED_PACKAGE, old)
811     }
812 
removednull813     override fun removed(old: ClassItem, from: Item?) {
814         val error = when {
815             old.isInterface() -> Issues.REMOVED_INTERFACE
816             old.deprecated -> Issues.REMOVED_DEPRECATED_CLASS
817             else -> Issues.REMOVED_CLASS
818         }
819 
820         handleRemoved(error, old)
821     }
822 
removednull823     override fun removed(old: MethodItem, from: ClassItem?) {
824         // See if there's a member from inherited class
825         val inherited = if (old.isConstructor()) {
826             null
827         } else {
828             // This can also return self, specially handled below
829             from?.findMethod(
830                 old,
831                 includeSuperClasses = true,
832                 includeInterfaces = from.isInterface()
833             )
834         }
835         if (inherited == null || inherited != old && inherited.isHiddenOrRemoved()) {
836             val error = if (old.deprecated) Issues.REMOVED_DEPRECATED_METHOD else Issues.REMOVED_METHOD
837             handleRemoved(error, old)
838         }
839     }
840 
removednull841     override fun removed(old: FieldItem, from: ClassItem?) {
842         val inherited = from?.findField(
843             old.name(),
844             includeSuperClasses = true,
845             includeInterfaces = from.isInterface()
846         )
847         if (inherited == null) {
848             val error = if (old.deprecated) Issues.REMOVED_DEPRECATED_FIELD else Issues.REMOVED_FIELD
849             handleRemoved(error, old)
850         }
851     }
852 
reportnull853     private fun report(
854         issue: Issue,
855         item: Item,
856         message: String
857     ) {
858         if (reporter.report(issue, item, message) && configuration.getSeverity(issue) == Severity.ERROR) {
859             foundProblems = true
860         }
861     }
862 
863     companion object {
checkCompatibilitynull864         fun checkCompatibility(
865             codebase: Codebase,
866             previous: Codebase,
867             releaseType: ReleaseType,
868             apiType: ApiType,
869             base: Codebase? = null
870         ) {
871             val filter = apiType.getEmitFilter()
872             val checker = CompatibilityCheck(filter, previous, apiType, base, getReporterForReleaseType(releaseType))
873             val issueConfiguration = releaseType.getIssueConfiguration()
874             val previousConfiguration = configuration
875             try {
876                 configuration = issueConfiguration
877                 CodebaseComparator().compare(checker, previous, codebase, filter)
878             } finally {
879                 configuration = previousConfiguration
880             }
881 
882             val message = "Aborting: Found compatibility problems checking " +
883                 "the ${apiType.displayName} API against the API in ${previous.location}"
884 
885             if (checker.foundProblems) {
886                 throw DriverException(exitCode = -1, stderr = message)
887             }
888         }
889 
getReporterForReleaseTypenull890         private fun getReporterForReleaseType(releaseType: ReleaseType): Reporter = when (releaseType) {
891             ReleaseType.DEV -> options.reporterCompatibilityCurrent
892             ReleaseType.RELEASED -> options.reporterCompatibilityReleased
893         }
894     }
895 }
896