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.model.AnnotationItem
20 import com.android.tools.metalava.model.Codebase
21 import com.android.tools.metalava.model.FieldItem
22 import com.android.tools.metalava.model.Item
23 import com.android.tools.metalava.model.MethodItem
24 import com.android.tools.metalava.model.ParameterItem
25 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
26 import com.android.tools.metalava.model.TypeItem
27 import com.android.tools.metalava.model.findAnnotation
28 import com.android.tools.metalava.model.hasAnnotation
29 
30 /**
31  * Performs null migration analysis, looking at previous API signature files and new signature
32  * files, and replacing new @Nullable and @NonNull annotations with @RecentlyNullable
33  * and @RecentlyNonNull.
34  *
35  * TODO: Enforce compatibility across type use annotations, e.g. changing parameter value from
36  *   {@code @NonNull List<@Nullable String>} to {@code @NonNull List<@NonNull String>} is forbidden.
37  */
38 class NullnessMigration : ComparisonVisitor(visitAddedItemsRecursively = true) {
comparenull39     override fun compare(old: Item, new: Item) {
40         if (hasNullnessInformation(new) && !hasNullnessInformation(old)) {
41             new.markRecent()
42         }
43     }
44 
45     // Note: We don't override added(new: Item) to mark newly added methods as newly
46     // having nullness annotations: those APIs are themselves new, so there's no reason
47     // to mark the nullness contract as migration (warning- rather than error-severity)
48 
comparenull49     override fun compare(old: MethodItem, new: MethodItem) {
50         @Suppress("ConstantConditionIf")
51         if (SUPPORT_TYPE_USE_ANNOTATIONS) {
52             val newType = new.returnType()
53             val oldType = old.returnType()
54             checkType(oldType, newType)
55         }
56     }
57 
comparenull58     override fun compare(old: FieldItem, new: FieldItem) {
59         @Suppress("ConstantConditionIf")
60         if (SUPPORT_TYPE_USE_ANNOTATIONS) {
61             val newType = new.type()
62             val oldType = old.type()
63             checkType(oldType, newType)
64         }
65     }
66 
comparenull67     override fun compare(old: ParameterItem, new: ParameterItem) {
68         @Suppress("ConstantConditionIf")
69         if (SUPPORT_TYPE_USE_ANNOTATIONS) {
70             val newType = new.type()
71             val oldType = old.type()
72             checkType(oldType, newType)
73         }
74     }
75 
76     @Suppress("UNUSED_PARAMETER")
hasNullnessInformationnull77     private fun hasNullnessInformation(type: TypeItem): Boolean {
78         return if (SUPPORT_TYPE_USE_ANNOTATIONS) {
79             // TODO: support type use
80             false
81         } else {
82             false
83         }
84     }
85 
86     @Suppress("UNUSED_PARAMETER")
checkTypenull87     private fun checkType(old: TypeItem, new: TypeItem) {
88         if (hasNullnessInformation(new)) {
89             assert(SUPPORT_TYPE_USE_ANNOTATIONS)
90             // TODO: support type use
91         }
92     }
93 
94     companion object {
migrateNullsnull95         fun migrateNulls(codebase: Codebase, previous: Codebase) {
96             CodebaseComparator(
97                     apiVisitorConfig = @Suppress("DEPRECATION") options.apiVisitorConfig,
98                 )
99                 .compare(NullnessMigration(), previous, codebase)
100         }
101 
hasNullnessInformationnull102         fun hasNullnessInformation(item: Item): Boolean {
103             return isNullable(item) || isNonNull(item)
104         }
105 
findNullnessAnnotationnull106         fun findNullnessAnnotation(item: Item): AnnotationItem? {
107             return item.modifiers.findAnnotation(AnnotationItem::isNullnessAnnotation)
108         }
109 
isNullablenull110         private fun isNullable(item: Item): Boolean {
111             return item.modifiers.hasAnnotation(AnnotationItem::isNullable)
112         }
113 
isNonNullnull114         private fun isNonNull(item: Item): Boolean {
115             return item.modifiers.hasAnnotation(AnnotationItem::isNonNull)
116         }
117     }
118 }
119 
120 /**
121  * Marks the nullability of this Item as Recent. That is, replaces @Nullable/@NonNull
122  * with @RecentlyNullable/@RecentlyNonNull
123  */
Itemnull124 fun Item.markRecent() {
125     val annotation = NullnessMigration.findNullnessAnnotation(this) ?: return
126     // Nullness information change: Add migration annotation
127     val annotationClass = if (annotation.isNullable()) RECENTLY_NULLABLE else RECENTLY_NONNULL
128 
129     val modifiers = mutableModifiers()
130     modifiers.removeAnnotation(annotation)
131 
132     modifiers.addAnnotation(codebase.createAnnotation("@$annotationClass", this))
133 }
134