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