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