1 /*
2  * Copyright 2019 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 @file:Suppress("UnstableApiUsage")
17 
18 package androidx.build.lint
19 
20 import com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_API
21 import com.android.tools.lint.detector.api.ApiConstraint
22 import com.android.tools.lint.detector.api.Category
23 import com.android.tools.lint.detector.api.Detector
24 import com.android.tools.lint.detector.api.Implementation
25 import com.android.tools.lint.detector.api.Incident
26 import com.android.tools.lint.detector.api.Issue
27 import com.android.tools.lint.detector.api.JavaContext
28 import com.android.tools.lint.detector.api.Scope
29 import com.android.tools.lint.detector.api.Severity
30 import com.android.tools.lint.detector.api.SourceCodeScanner
31 import com.android.tools.lint.detector.api.VersionChecks.Companion.isPrecededByVersionCheckExit
32 import com.android.tools.lint.detector.api.VersionChecks.Companion.isWithinVersionCheckConditional
33 import com.intellij.psi.PsiAnnotation
34 import com.intellij.psi.PsiMethod
35 import org.jetbrains.uast.UCallExpression
36 import org.jetbrains.uast.UExpression
37 import org.jetbrains.uast.getContainingUClass
38 import org.jetbrains.uast.getContainingUMethod
39 
40 class BanUncheckedReflection : Detector(), SourceCodeScanner {
41 
getApplicableMethodNamesnull42     override fun getApplicableMethodNames() = listOf(METHOD_INVOKE_NAME)
43 
44     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
45         // We don't care if the invocation is correct -- there's another lint for that. We're
46         // just enforcing the "all reflection on the platform SDK must be gated on SDK_INT checks"
47         // policy. Also -- since we're not actually checking whether the invocation is on the
48         // platform SDK -- we're discouraging reflection in general.
49 
50         // Skip if this isn't a call to `Method.invoke`.
51         if (!context.evaluator.isMemberInClass(method, METHOD_REFLECTION_CLASS)) return
52 
53         // Flag if the call isn't inside or preceded by an SDK_INT check.
54         if (
55             !isWithinVersionCheckConditional(
56                 context,
57                 node,
58                 ApiConstraint.get(HIGHEST_KNOWN_API),
59                 false
60             ) &&
61                 !isWithinVersionCheckConditional(context, node, ApiConstraint.get(1), true) &&
62                 !isPrecededByVersionCheckExit(
63                     context,
64                     node,
65                     ApiConstraint.get(HIGHEST_KNOWN_API)
66                 ) &&
67                 !isPrecededByVersionCheckExit(context, node, ApiConstraint.get(1)) &&
68                 !isWithinDeprecatedSinceApiMethod(node) &&
69                 !isWithinDeprecatedSinceApiClass(node)
70         ) {
71             val incident =
72                 Incident(context)
73                     .issue(ISSUE)
74                     .location(context.getLocation(node))
75                     .message(
76                         "Method.invoke requires both an upper and lower SDK bounds checks to be" +
77                             " safe, and the upper bound must be below SdkVersionInfo.HIGHEST_KNOWN_API."
78                     )
79                     .scope(node)
80             context.report(incident)
81         }
82     }
83 
84     /** Checks if the expression is within a method annotated with @DeprecatedSinceApi. */
isWithinDeprecatedSinceApiMethodnull85     private fun isWithinDeprecatedSinceApiMethod(node: UExpression): Boolean {
86         val containingMethod = node.getContainingUMethod() ?: return false
87         return annotationsContainDeprecatedSinceApi(containingMethod.annotations)
88     }
89 
90     /** Checks if the expression is within a class annotated with @DeprecatedSinceApi. */
isWithinDeprecatedSinceApiClassnull91     private fun isWithinDeprecatedSinceApiClass(node: UExpression): Boolean {
92         val containingClass = node.getContainingUClass() ?: return false
93         return annotationsContainDeprecatedSinceApi(containingClass.annotations)
94     }
95 
96     /** Checks if any of the annotations are @DeprecatedSinceApi. */
annotationsContainDeprecatedSinceApinull97     private fun annotationsContainDeprecatedSinceApi(annotations: Array<PsiAnnotation>): Boolean {
98         for (annotation in annotations) {
99             if (annotation.hasQualifiedName(DEPRECATED_SINCE_API_ANNOTATION)) {
100                 return true
101             }
102         }
103         return false
104     }
105 
106     companion object {
107         val ISSUE =
108             Issue.create(
109                 "BanUncheckedReflection",
110                 "Reflection that is not within an SDK check",
111                 "Jetpack policy discourages reflection. In cases where reflection is used on " +
112                     "platform SDK classes, it must be used within an `SDK_INT` check that delegates " +
113                     "to an equivalent public API on the latest version of the platform. If no " +
114                     "equivalent public API exists, reflection must not be used. For more " +
115                     "information, see go/androidx-api-guidelines#sdk-reflection.",
116                 Category.CORRECTNESS,
117                 5,
118                 Severity.ERROR,
119                 Implementation(BanUncheckedReflection::class.java, Scope.JAVA_FILE_SCOPE)
120             )
121 
122         const val METHOD_REFLECTION_CLASS = "java.lang.reflect.Method"
123         const val METHOD_INVOKE_NAME = "invoke"
124         const val DEPRECATED_SINCE_API_ANNOTATION = "androidx.annotation.DeprecatedSinceApi"
125     }
126 }
127