1 /*
<lambda>null2  * Copyright 2022 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 androidx.build.lint
18 
19 import com.android.tools.lint.client.api.UElementHandler
20 import com.android.tools.lint.detector.api.Category
21 import com.android.tools.lint.detector.api.Detector
22 import com.android.tools.lint.detector.api.Implementation
23 import com.android.tools.lint.detector.api.Incident
24 import com.android.tools.lint.detector.api.Issue
25 import com.android.tools.lint.detector.api.JavaContext
26 import com.android.tools.lint.detector.api.LocationType
27 import com.android.tools.lint.detector.api.Scope
28 import com.android.tools.lint.detector.api.Severity
29 import com.android.tools.lint.detector.api.SourceCodeScanner
30 import com.android.tools.lint.detector.api.isKotlin
31 import com.android.tools.lint.model.LintModelMavenName
32 import com.intellij.psi.PsiJvmModifiersOwner
33 import org.jetbrains.uast.UClass
34 import org.jetbrains.uast.UMethod
35 
36 /**
37  * This lint check is meant to help maintain binary compatibility in a one-time transition to using
38  * `-Xjvm-default=all`. Applicable interfaces which existed before `-Xjvm-default=all` was used must
39  * be annotated with @JvmDefaultWithCompatibility. However, after the initial change, new interfaces
40  * should not use @JvmDefaultWithCompatibility.
41  *
42  * Because this check is only meant to be used once, it should not be added to the issue registry.
43  */
44 class MissingJvmDefaultWithCompatibilityDetector : Detector(), SourceCodeScanner {
45 
46     override fun getApplicableUastTypes() = listOf(UClass::class.java)
47 
48     override fun createUastHandler(context: JavaContext): UElementHandler {
49         return InterfaceChecker(context)
50     }
51 
52     fun LintModelMavenName.asProjectString() = "$groupId.$artifactId"
53 
54     private inner class InterfaceChecker(val context: JavaContext) : UElementHandler() {
55         override fun visitClass(node: UClass) {
56             // Don't run lint on the set of projects that already used `-Xjvm-default=all` before
57             // all projects were switched over.
58             if (alreadyDefaultAll.contains(context.project.mavenCoordinate?.asProjectString())) {
59                 return
60             }
61 
62             if (!isKotlin(node.language)) return
63             if (!node.isInterface) return
64             if (
65                 node.annotatedWithAnyOf(
66                     // If the interface is not stable, it doesn't need the annotation
67                     BanInappropriateExperimentalUsage.APPLICABLE_ANNOTATIONS +
68                         // If the interface already has the annotation, it doesn't need it again
69                         JVM_DEFAULT_WITH_COMPATIBILITY
70                 )
71             )
72                 return
73 
74             val stableMethods = node.stableMethods()
75             if (stableMethods.any { it.hasDefaultImplementation() }) {
76                 val reason =
77                     "This interface must be annotated with @JvmDefaultWithCompatibility " +
78                         "because it has a stable method with a default implementation"
79                 reportIncident(node, reason)
80                 return
81             }
82 
83             if (stableMethods.any { it.hasParameterWithDefaultValue() }) {
84                 val reason =
85                     "This interface must be annotated with @JvmDefaultWithCompatibility " +
86                         "because it has a stable method with a parameter with a default value"
87                 reportIncident(node, reason)
88                 return
89             }
90 
91             // This only checks the interfaces that this interface directly extends, which means if
92             // A extends B extends C and C is @JvmDefaultWithCompatibility, there will need to be
93             // two passes of running the check to annotate A and B.
94             if (
95                 node.interfaces.any {
96                     it.annotatedWithAnyOf(listOf(JVM_DEFAULT_WITH_COMPATIBILITY))
97                 }
98             ) {
99                 val reason =
100                     "This interface must be annotated with @JvmDefaultWithCompatibility " +
101                         "because it implements an interface which uses this annotation"
102                 reportIncident(node, reason)
103                 return
104             }
105         }
106 
107         private fun reportIncident(node: UClass, reason: String) {
108             val fix =
109                 fix()
110                     .name("Annotate with @JvmDefaultWithCompatibility")
111                     .annotate(JVM_DEFAULT_WITH_COMPATIBILITY, context, node)
112                     .autoFix()
113                     .build()
114 
115             val incident =
116                 Incident(context)
117                     .fix(fix)
118                     .issue(ISSUE)
119                     .location(context.getLocation(node, LocationType.ALL))
120                     .message(reason)
121                     .scope(node)
122 
123             context.report(incident)
124         }
125 
126         /** Returns a list of the class's stable methods (methods not labelled as experimental). */
127         private fun UClass.stableMethods(): List<UMethod> =
128             methods.filter {
129                 !it.annotatedWithAnyOf(BanInappropriateExperimentalUsage.APPLICABLE_ANNOTATIONS)
130             }
131 
132         /**
133          * Checks if the element is annotated with any of the provided (fully qualified) annotation
134          * names. This uses `PsiJvmModifiersOwner` because it seems to be the one common parent of
135          * `UClass` and `UMethod` with an `annotations` property.
136          */
137         private fun PsiJvmModifiersOwner.annotatedWithAnyOf(
138             qualifiedAnnotationNames: List<String>
139         ): Boolean = annotations.any { qualifiedAnnotationNames.contains(it.qualifiedName) }
140 
141         private fun UMethod.hasDefaultImplementation(): Boolean = uastBody != null
142 
143         private fun UMethod.hasParameterWithDefaultValue(): Boolean =
144             uastParameters.any { param -> param.uastInitializer != null }
145     }
146 
147     companion object {
148         val ISSUE =
149             Issue.create(
150                 "MissingJvmDefaultWithCompatibility",
151                 "The @JvmDefaultWithCompatibility needs to be used with on applicable " +
152                     "interfaces when `-Xjvm-default=all` is turned on to preserve compatibility.",
153                 "Libraries that pass `-Xjvm-default=all` to the Kotlin compiler must " +
154                     "use the @JvmDefaultWithCompatibility annotation on previously existing " +
155                     "interfaces with stable methods with default implementations or default parameter" +
156                     " values, and interfaces that extend other @JvmDefaultWithCompatibility " +
157                     "interfaces. See go/androidx-api-guidelines#kotlin-jvm-default for more details.",
158                 Category.CORRECTNESS,
159                 5,
160                 Severity.ERROR,
161                 Implementation(
162                     MissingJvmDefaultWithCompatibilityDetector::class.java,
163                     Scope.JAVA_FILE_SCOPE
164                 )
165             )
166 
167         const val JVM_DEFAULT_WITH_COMPATIBILITY = "kotlin.jvm.JvmDefaultWithCompatibility"
168 
169         // This set of projects was created by running `grep "Xjvm-default=all" . -r` in the
170         // `frameworks/support` directory and converting the `build.gradle` files in that list to
171         // this format.
172         private val alreadyDefaultAll =
173             setOf(
174                 "androidx.room.room-compiler-processing",
175                 "androidx.room.room-migration",
176                 "androidx.room.room-testing",
177                 "androidx.room.room-compiler",
178                 "androidx.room.room-ktx",
179                 "androidx.room.room-common",
180                 "androidx.room.room-runtime",
181                 "androidx.compose.ui.ui",
182                 "androidx.compose.ui.ui-unit",
183                 "androidx.compose.ui.ui-tooling-preview",
184                 "androidx.compose.ui.ui-tooling-data",
185                 "androidx.compose.ui.ui-util",
186                 "androidx.compose.ui.ui-test",
187                 "androidx.compose.ui.ui-test-manifest",
188                 "androidx.compose.ui.ui-inspection",
189                 "androidx.compose.ui.ui-viewbinding",
190                 "androidx.compose.ui.ui-geometry",
191                 "androidx.compose.ui.ui-graphics",
192                 "androidx.compose.ui.ui-text",
193                 "androidx.compose.ui.ui-text-google-fonts",
194                 "androidx.compose.ui.ui-test-junit4",
195                 "androidx.compose.ui.ui-tooling",
196                 "androidx.compose.test-utils",
197                 "androidx.compose.runtime.runtime",
198                 "androidx.compose.runtime.runtime-livedata",
199                 "androidx.compose.runtime.runtime-saveable",
200                 "androidx.compose.runtime.runtime-rxjava2",
201                 "androidx.compose.runtime.runtime-tracing",
202                 "androidx.compose.runtime.runtime-rxjava3",
203                 "androidx.compose.animation.animation-tooling-internal",
204                 "androidx.compose.animation.animation",
205                 "androidx.compose.animation.animation-graphics",
206                 "androidx.compose.animation.animation-core",
207                 "androidx.compose.foundation.foundation",
208                 "androidx.compose.foundation.foundation-layout",
209                 "androidx.compose.material3.material3-window-size-class",
210                 "androidx.compose.material3.material3.integration-tests.material3-catalog",
211                 "androidx.compose.material3.material3",
212                 "androidx.compose.material.material-ripple",
213                 "androidx.lifecycle.lifecycle-viewmodel",
214                 "androidx.sqlite.sqlite-ktx",
215                 "androidx.sqlite.sqlite-framework",
216                 "androidx.sqlite.integration-tests.inspection-sqldelight-testapp",
217                 "androidx.sqlite.integration-tests.inspection-room-testapp",
218                 "androidx.sqlite.sqlite",
219                 "androidx.sqlite.sqlite-inspection",
220                 "androidx.tv.tv-foundation",
221                 "androidx.tv.tv-material",
222                 "androidx.window.window",
223                 "androidx.credentials.credentials",
224                 "androidx.wear.compose.compose-material",
225                 "androidx.wear.watchface.watchface-complications-data-source",
226                 "androidx.wear.watchface.watchface",
227                 "androidx.wear.watchface.watchface-client",
228                 "androidx.lifecycle.lifecycle-common",
229                 // These projects didn't already have "Xjvm-default=al", but the only have the error
230                 // in
231                 // integration tests, where the annotation isn't needed.
232                 "androidx.annotation.annotation-experimental-lint-integration-tests",
233                 "androidx.annotation.annotation-experimental-lint",
234                 "androidx.camera.integration-tests.camera-testapp-camera2-pipe",
235                 "androidx.compose.integration-tests.docs-snippets",
236                 // These projects are excluded due to b/259578592
237                 "androidx.camera.camera-camera2-pipe",
238                 "androidx.camera.camera-camera2-pipe-integration",
239                 "androidx.camera.camera-camera2-pipe-testing",
240             )
241     }
242 }
243