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