1 /* <lambda>null2 * Copyright 2025 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.SdkConstants.ATTR_VALUE 20 import com.android.tools.lint.detector.api.AnnotationInfo 21 import com.android.tools.lint.detector.api.AnnotationOrigin 22 import com.android.tools.lint.detector.api.AnnotationUsageInfo 23 import com.android.tools.lint.detector.api.AnnotationUsageType 24 import com.android.tools.lint.detector.api.Category 25 import com.android.tools.lint.detector.api.Detector 26 import com.android.tools.lint.detector.api.Implementation 27 import com.android.tools.lint.detector.api.Issue 28 import com.android.tools.lint.detector.api.JavaContext 29 import com.android.tools.lint.detector.api.Scope 30 import com.android.tools.lint.detector.api.Severity 31 import com.android.tools.lint.detector.api.SourceCodeScanner 32 import com.android.tools.lint.detector.api.getMethodName 33 import com.android.tools.lint.detector.api.isUnconditionalReturn 34 import com.intellij.psi.PsiClass 35 import com.intellij.psi.PsiField 36 import com.intellij.psi.PsiLiteralValue 37 import com.intellij.psi.PsiMethod 38 import com.intellij.psi.PsiNamedElement 39 import org.jetbrains.uast.UAnnotated 40 import org.jetbrains.uast.UAnnotation 41 import org.jetbrains.uast.UBlockExpression 42 import org.jetbrains.uast.UCallExpression 43 import org.jetbrains.uast.UClassLiteralExpression 44 import org.jetbrains.uast.UElement 45 import org.jetbrains.uast.UFile 46 import org.jetbrains.uast.UIfExpression 47 import org.jetbrains.uast.UMethod 48 import org.jetbrains.uast.UParenthesizedExpression 49 import org.jetbrains.uast.UPolyadicExpression 50 import org.jetbrains.uast.UReferenceExpression 51 import org.jetbrains.uast.USwitchClauseExpression 52 import org.jetbrains.uast.UUnaryExpression 53 import org.jetbrains.uast.UastBinaryOperator 54 import org.jetbrains.uast.UastFacade 55 import org.jetbrains.uast.UastPrefixOperator 56 import org.jetbrains.uast.evaluateString 57 import org.jetbrains.uast.skipParenthesizedExprDown 58 import org.jetbrains.uast.toUElement 59 import org.jetbrains.uast.tryResolve 60 import org.jetbrains.uast.util.isConstructorCall 61 62 /** 63 * Enforced flag checking in the Android platform; see go/android-flagged-apis. 64 * 65 * **NOTE:** This file is a fork of the original sources in the Android lint code base, see 66 * `lint-checks/src/main/java/com/android/tools/lint/checks/optional/FlaggedApiDetector.kt` 67 */ 68 class FlaggedApiDetector : Detector(), SourceCodeScanner { 69 companion object Issues { 70 private val IMPLEMENTATION = 71 Implementation(FlaggedApiDetector::class.java, Scope.JAVA_FILE_SCOPE) 72 73 /** Accessing flagged api without check. */ 74 @JvmField 75 val ISSUE = 76 Issue.create( 77 id = "AndroidXFlaggedApi", 78 explanation = 79 """ 80 This lint check looks for accesses of APIs marked with `@FlaggedApi(X)` without \ 81 a guarding `if (Flags.X)` check or equivalent gating check. See go/android-flagged-apis. 82 """, 83 briefDescription = "FlaggedApi access without check", 84 category = Category.CORRECTNESS, 85 priority = 6, 86 severity = Severity.ERROR, 87 androidSpecific = true, 88 implementation = IMPLEMENTATION, 89 ) 90 91 private const val CHECKS_ACONFIG_FLAG_ANNOTATION = "androidx.annotation.ChecksAconfigFlag" 92 private const val ATTR_FLAG = "flag" 93 private const val FLAGGED_API_ANNOTATION = "android.annotation.FlaggedApi" 94 } 95 96 override fun applicableAnnotations(): List<String> { 97 return listOf(FLAGGED_API_ANNOTATION) 98 } 99 100 override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean { 101 return when (type) { 102 AnnotationUsageType.METHOD_CALL, 103 AnnotationUsageType.METHOD_REFERENCE, 104 AnnotationUsageType.FIELD_REFERENCE, 105 AnnotationUsageType.CLASS_REFERENCE, 106 AnnotationUsageType.ANNOTATION_REFERENCE, 107 AnnotationUsageType.EXTENDS, 108 AnnotationUsageType.DEFINITION -> true 109 else -> false 110 } 111 } 112 113 override fun inheritAnnotation(annotation: String): Boolean { 114 return false 115 } 116 117 override fun visitAnnotationUsage( 118 context: JavaContext, 119 element: UElement, 120 annotationInfo: AnnotationInfo, 121 usageInfo: AnnotationUsageInfo, 122 ) { 123 val flagString = getFlaggedApiString(annotationInfo.annotation) 124 if (flagString == null) { 125 context.report( 126 ISSUE, 127 element, 128 context.getLocation(element), 129 "Failed to obtain flag string" 130 ) 131 return 132 } 133 134 // Avoid checking usage of the `@FlaggedApi` annotation itself. This should only happen in 135 // tests, since in practice we only define flagged APIs inside the platform SDK. 136 if ( 137 annotationInfo.origin == AnnotationOrigin.SELF && 138 annotationInfo.qualifiedName == FLAGGED_API_ANNOTATION 139 ) { 140 return 141 } 142 143 // Avoid checking flagged deprecations. We don't allow adding APIs as deprecated, so we can 144 // safely assume that the flag applies to the deprecated state rather than the API itself. 145 if (isFlaggedDeprecation(usageInfo)) return 146 147 // Only allowlisted libraries are allowed to call flagged APIs. 148 if (!isUsageInAllowlistedLibrary(context, usageInfo.usage)) { 149 context.report( 150 ISSUE, 151 element, 152 context.getLocation(element), 153 "Flagged APIs are subject to additional policies and may only be called by " + 154 "libraries that have been allowlisted by Jetpack Working Group" 155 ) 156 return 157 } 158 159 // Only alpha libraries are allowed to call flagged APIs. 160 if (!isUsageInAlphaLibrary(context, usageInfo.usage)) { 161 context.report( 162 ISSUE, 163 element, 164 context.getLocation(element), 165 "Flagged APIs may only be called during alpha and must be removed before moving " + 166 "to beta" 167 ) 168 return 169 } 170 171 // Is the usage checked? Great. 172 if (isFlagChecked(element, flagString)) return 173 174 val referenced = element.tryResolve() 175 val description = 176 when { 177 referenced is PsiMethod -> "Method `${referenced.name}()`" 178 element is UCallExpression -> 179 if (element.isConstructorCall()) { 180 val className = (element.classReference?.tryResolve() as? PsiClass)?.name 181 "Constructor for class `$className`" 182 } else { 183 "Method `${getMethodName(element)}()`" 184 } 185 referenced is PsiField -> "Field `${referenced.name}`" 186 referenced is PsiClass -> "Class `${referenced.name}`" 187 element is UClassLiteralExpression -> 188 "Class `${element.expression?.sourcePsi?.text}`" 189 referenced is PsiNamedElement -> "Reference `${referenced.name}`" 190 else -> "This" 191 } 192 val message = 193 "$description is a flagged API and must be inside a flag check for \"$flagString\"" 194 context.report(ISSUE, element, context.getLocation(element), message) 195 } 196 197 private val AnnotationUsageInfo.referencedElement: UElement? 198 get() = 199 referenced.toUElement() 200 ?: if ((usage as? UCallExpression)?.isConstructorCall() == true) { 201 (usage as UCallExpression).classReference?.tryResolve().toUElement() 202 } else { 203 null 204 } 205 206 private fun isUsageInAllowlistedLibrary(context: JavaContext, usage: UElement): Boolean = 207 (context.evaluator.getLibrary(usage) ?: context.project.mavenCoordinate)?.let { 208 allowlistedCoordinates.contains(it.groupId) || 209 allowlistedCoordinates.contains("${it.groupId}:${it.artifactId}") 210 } ?: true // If we can't obtain the Maven coordinate, assume we're in a lint test. 211 212 private fun isUsageInAlphaLibrary(context: JavaContext, usage: UElement): Boolean = 213 (context.evaluator.getLibrary(usage) ?: context.project.mavenCoordinate) 214 ?.version 215 ?.contains("-alpha") 216 ?: true // If we can't obtain the Maven coordinate, assume we're in a lint test. 217 218 private fun isFlaggedDeprecation(usageInfo: AnnotationUsageInfo): Boolean = 219 (usageInfo.referencedElement as? UAnnotated)?.let { 220 it.findAnnotation("java.lang.Deprecated") != null || 221 it.findAnnotation("kotlin.Deprecated") != null 222 } == true 223 224 private fun getFlaggedApiString(annotation: UAnnotation): String? = 225 (annotation.javaPsi?.findAttributeValue(ATTR_VALUE) as? PsiLiteralValue)?.value as? String 226 227 /** Is the given [element] inside a flag check? */ 228 private fun isFlagChecked( 229 element: UElement, 230 flagString: String, 231 ): Boolean { 232 var curr = element.uastParent ?: return false 233 234 var prev = element 235 while (curr !is UFile) { 236 if (curr is UIfExpression) { 237 val condition = curr.condition 238 if (prev !== condition) { 239 val fromThen = prev == curr.thenExpression 240 if (fromThen) { 241 if (isFlagExpression(condition, flagString)) { 242 return true 243 } 244 } else { 245 // Handle "if (!Flags.X) else <CALL>" 246 val op = condition.skipParenthesizedExprDown() 247 if ( 248 op is UUnaryExpression && 249 op.operator == UastPrefixOperator.LOGICAL_NOT && 250 isFlagExpression(op.operand, flagString) 251 ) { 252 return true 253 } else if ( 254 op is UPolyadicExpression && 255 op.operator == UastBinaryOperator.LOGICAL_OR && 256 (op.operands.any { 257 val nested = it.skipParenthesizedExprDown() 258 nested is UUnaryExpression && 259 nested.operator == UastPrefixOperator.LOGICAL_NOT && 260 isFlagExpression(nested.operand, flagString) 261 }) 262 ) { 263 return true 264 } 265 } 266 } 267 } else if (curr is USwitchClauseExpression) { 268 if (curr.caseValues.any { value -> isFlagExpression(value, flagString) }) { 269 return true 270 } 271 } else if ( 272 curr is UPolyadicExpression && curr.operator == UastBinaryOperator.LOGICAL_AND 273 ) { 274 for (operand in curr.operands) { 275 if (operand === curr) { 276 break 277 } else if (isFlagExpression(operand, flagString)) { 278 return true 279 } 280 } 281 } else if (curr is UMethod) { 282 // See if there's an early return. We *only* handle a very simple canonical format 283 // here; 284 // must be first statement in method. 285 val body = curr.uastBody 286 if (body is UBlockExpression && body.expressions.size > 1) { 287 val first = body.expressions[0] 288 if (first is UIfExpression) { 289 val condition = first.condition.skipParenthesizedExprDown() 290 if ( 291 condition is UUnaryExpression && 292 condition.operator == UastPrefixOperator.LOGICAL_NOT && 293 isFlagExpression(condition.operand, flagString) 294 ) { 295 // It's a flag check; make sure we just return 296 val then = first.thenExpression?.skipParenthesizedExprDown() 297 if (then != null && then.isUnconditionalReturn()) { 298 return true 299 } 300 } 301 } 302 } 303 } 304 305 prev = curr 306 curr = curr.uastParent ?: break 307 } 308 309 return false 310 } 311 312 /** Is the given [element] a flag expression (e.g. "Flags.set()") or equivalently annotated? */ 313 private fun isFlagExpression( 314 element: UElement, 315 flagString: String, 316 ): Boolean { 317 if (element is UUnaryExpression && element.operator == UastPrefixOperator.LOGICAL_NOT) { 318 return !isFlagExpression(element.operand, flagString) 319 } else if (element is UReferenceExpression || element is UCallExpression) { 320 val resolved = element.tryResolve() 321 if (resolved is PsiMethod) { 322 if ( 323 (resolved.toUElement() as UAnnotated) 324 .uAnnotations 325 .filter { it.qualifiedName == CHECKS_ACONFIG_FLAG_ANNOTATION } 326 .mapNotNull { 327 val attr = 328 it.findAttributeValue(ATTR_FLAG) 329 ?: it.findAttributeValue(null) 330 ?: return@mapNotNull null 331 attr.evaluateString() 332 ?: (attr.javaPsi as? PsiLiteralValue)?.value as? String 333 } 334 .contains(flagString) 335 ) { 336 return true 337 } 338 } else if (resolved is PsiField) { 339 // Arguably we should look for final fields here, but on the other hand 340 // there may be cases where it's initialized later, so it's a bit like 341 // Kotlin's "lateinit". Treat them all as constant. 342 val initializer = UastFacade.getInitializerBody(resolved) 343 if (initializer != null) { 344 return isFlagExpression(initializer, flagString) 345 } 346 } 347 } else if (element is UParenthesizedExpression) { 348 return isFlagExpression(element.expression, flagString) 349 } else if (element is UPolyadicExpression) { 350 if (element.operator == UastBinaryOperator.LOGICAL_AND) { 351 for (operand in element.operands) { 352 if (isFlagExpression(operand, flagString)) { 353 return true 354 } 355 } 356 } 357 } 358 return false 359 } 360 } 361 362 // List of libraries which are allowed to call flagged APIs, where `groupId:artifactId` represents a 363 // single module and `groupId` represents an entire group of modules. 364 private val allowlistedCoordinates = 365 listOf( 366 "test", 367 "androidx.mediarouter", 368 ) 369