blob: 9c0183d900cfd19b0088e6faee547199c631ec34 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks
import com.android.sdklib.AndroidVersion
import com.android.tools.lint.client.api.LintClient
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.ConstantEvaluator
import com.android.tools.lint.detector.api.Context
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.TypeEvaluator
import com.android.tools.lint.detector.api.UastLintUtils
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementFactory
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiPrimitiveType
import com.intellij.psi.PsiType
import com.intellij.psi.PsiVariable
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClassLiteralExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
/**
* Checks that the code is not using reflection to access hidden Android APIs
*/
class PrivateApiDetector : Detector(), SourceCodeScanner {
companion object Issues {
/** Using hidden/private APIs */
@JvmField
val PRIVATE_API = Issue.create(
id = "PrivateApi",
briefDescription = "Using Private APIs",
explanation = """
Using reflection to access hidden/private Android APIs is not safe; it will often not work on \
devices from other vendors, and it may suddenly stop working (if the API is removed) or crash \
spectacularly (if the API behavior changes, since there are no guarantees for compatibility).
""",
moreInfo = "https://developer.android.com/preview/restrictions-non-sdk-interfaces",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.WARNING,
androidSpecific = true,
implementation = Implementation(
PrivateApiDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
@JvmField
val DISCOURAGED_PRIVATE_API = Issue.create(
id = "DiscouragedPrivateApi",
briefDescription = "Using Discouraged Private API",
explanation = """
Usage of restricted non-SDK interface may throw an exception at runtime. Accessing \
non-SDK methods or fields through reflection has a high likelihood to break your app \
between versions, and is being restricted to facilitate future app compatibility.
""",
moreInfo = "https://developer.android.com/preview/restrictions-non-sdk-interfaces",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.WARNING,
androidSpecific = true,
implementation = Implementation(
PrivateApiDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
@JvmField
val SOON_BLOCKED_PRIVATE_API = Issue.create(
id = "SoonBlockedPrivateApi",
briefDescription = "Using Soon-to-Be Blocked Private API",
explanation = """
Usage of restricted non-SDK interface will throw an exception at runtime. Accessing \
non-SDK methods or fields through reflection has a high likelihood to break your app \
between versions, and is being restricted to facilitate future app compatibility.
""",
moreInfo = "https://developer.android.com/preview/restrictions-non-sdk-interfaces",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.ERROR,
androidSpecific = true,
implementation = Implementation(
PrivateApiDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
@JvmField
val BLOCKED_PRIVATE_API = Issue.create(
id = "BlockedPrivateApi",
briefDescription = "Using Blocked Private API",
explanation = """
Usage of restricted non-SDK interface is forbidden for this targetSDK. Accessing \
non-SDK methods or fields through reflection has a high likelihood to break your app \
between versions, and is being restricted to facilitate future app compatibility.
""",
moreInfo = "https://developer.android.com/preview/restrictions-non-sdk-interfaces",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.FATAL,
androidSpecific = true,
implementation = Implementation(
PrivateApiDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
private const val LOAD_CLASS = "loadClass"
private const val FOR_NAME = "forName"
private const val GET_CLASS = "getClass"
private const val GET_DECLARED_CONSTRUCTOR = "getDeclaredConstructor"
private const val GET_DECLARED_METHOD = "getDeclaredMethod"
private const val GET_DECLARED_FIELD = "getDeclaredField"
private val KOTLIN_REFLECTION_METHODS = listOf(
"members", // this is available in kotlin-stdlib, the rest are in kotlin-reflect
"declaredMembers",
"declaredFunctions",
"declaredMemberFunctions",
"declaredMemberProperties"
)
private const val ERROR_MESSAGE = "Accessing internal APIs via reflection is not " +
"supported and may not work on all devices or in the future"
}
private var client: LintClient? = null
private var psiFactory: PsiElementFactory? = null
// Only initialize private API database on demand, at most once per Lint session.
private var cachedApiDatabase: Boolean = false
private var privateApiDatabase: PrivateApiLookup? = null
private val apiDatabase: PrivateApiLookup?
get() {
if (!cachedApiDatabase && privateApiDatabase == null && client != null) {
privateApiDatabase = PrivateApiLookup.get(client!!)
cachedApiDatabase = true
}
return privateApiDatabase
}
override fun beforeCheckRootProject(context: Context) {
client = context.client
cachedApiDatabase = false
psiFactory = PsiElementFactory.getInstance(context.project.ideaProject)
}
// ---- Implements JavaPsiScanner ----
override fun getApplicableMethodNames(): List<String>? =
listOf(
FOR_NAME,
LOAD_CLASS,
GET_DECLARED_CONSTRUCTOR,
GET_DECLARED_METHOD,
GET_DECLARED_FIELD
)
override fun getApplicableReferenceNames(): List<String>? = KOTLIN_REFLECTION_METHODS
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
val evaluator = context.evaluator
if (LOAD_CLASS == method.name) {
if (evaluator.isMemberInClass(method, "java.lang.ClassLoader") ||
evaluator.isMemberInClass(method, "dalvik.system.DexFile")
) {
checkLoadClass(context, node)
}
} else {
if (!evaluator.isMemberInClass(method, "java.lang.Class")) {
return
}
if (GET_DECLARED_METHOD == method.name) {
checkGetDeclaredMethod(context, node)
} else {
checkLoadClass(context, node)
}
}
}
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement
) {
// Kotlin reflection is harder to analyze statically, there are multiple ways of
// finally matching a method from the collection of declared members, etc. We heuristically
// try to match the strings we see with method and field names from the private API list.
// TODO: implement once Lint supports resolving property access.
}
private fun checkGetDeclaredMethod(context: JavaContext, call: UCallExpression) {
val cls = getJavaClassFromMemberLookup(call) ?: return
val arguments = call.valueArguments
if (arguments.isEmpty()) {
return
}
val methodName = ConstantEvaluator.evaluateString(context, arguments[0], false)
val aClass = context.evaluator.findClass(cls)
if (aClass != null && aClass.findMethodsByName(methodName, true).isNotEmpty()) {
return
}
val targetSdk = context.project.targetSdk
val location = context.getLocation(call)
if (targetSdk < AndroidVersion.VersionCodes.O) {
if (!(cls.startsWith("com.android.") || cls.startsWith("android."))) {
return
}
context.report(PRIVATE_API, call, location, ERROR_MESSAGE)
} else if (methodName != null) {
// When targetSDK is at least 28, we perform stricter checks against the API blacklist.
val argTypes =
if (arguments.size >= 2) arguments.subList(1, arguments.size)
.mapNotNull { getJavaClassType(it) }
.toTypedArray()
else
emptyArray()
val desc = context.evaluator
.constructMethodDescription(method = methodName, argumentTypes = argTypes) ?: return
val restriction = apiDatabase?.getMethodRestriction(cls, methodName, desc)
reportIssue(context, restriction, methodName, call)
}
}
private fun checkLoadClass(
context: JavaContext,
call: UCallExpression
) {
val arguments = call.valueArguments
if (arguments.isEmpty()) {
return
}
val value = ConstantEvaluator.evaluate(context, arguments[0]) as? String ?: return
var isInternal = false
if (value.startsWith("com.android.internal.")) {
isInternal = true
} else if (value.startsWith("com.android.") || value.startsWith("android.") &&
!value.startsWith("android.support.")
) {
// Attempting to access internal API? Look in two places:
// (1) SDK class
// (2) API database
val aClass = context.evaluator.findClass(value)
if (aClass != null) { // Found in SDK: not internal
return
}
val apiLookup = ApiLookup.get(
context.client,
context.mainProject.buildTarget
) ?: return
isInternal = !apiLookup.containsClass(value)
}
if (isInternal) {
val location = context.getLocation(call)
context.report(PRIVATE_API, call, location, ERROR_MESSAGE)
}
}
/**
* Given a Class#getMethodDeclaration or getFieldDeclaration etc call,
* figure out the corresponding class name the method is being invoked on
*
* @param call the [Class.getDeclaredMethod] or [Class.getDeclaredField] call
*
* @return the fully qualified name of the class, if found
*/
private fun getJavaClassFromMemberLookup(call: UCallExpression): String? =
getJavaClassType(call.receiver)?.canonicalText
/** We know [element] has type java.lang.Class<T> and we try to find out the PsiType for T. */
private fun getJavaClassType(element: UElement?): PsiType? {
if (element is UExpression) {
// First try the type inferred from the Psi, in case it's a known class reference.
val type = element.getExpressionType()
if (type is PsiClassType && type.parameterCount == 1) {
var clazz = type.parameters[0]
if (clazz is PsiClassType) {
PsiPrimitiveType.getUnboxedType(clazz)?.let {
// Make sure we extract the primitive type (int.class, Integer.TYPE in Java,
// Int::class.javaPrimitiveType in Kotlin)
if (element is UQualifiedReferenceExpression) {
val identifier =
(element.selector as? USimpleNameReferenceExpression)?.identifier
if (identifier == "javaPrimitiveType" || identifier == "TYPE") {
clazz = it
}
}
if (element is UClassLiteralExpression && element.evaluate() is PsiPrimitiveType) {
clazz = it
}
}
return clazz
}
// Here we might have a wildcard type, most likely an unbounded Class<?> coming from
// a loadClass or Class.forName() call. We can also have a bounded <? extends Foo>,
// from foo.getClass(), but if Foo is not final we cannot statically guarantee the
// receiver is indeed class Foo. So we fall-through to the handling below.
}
if (element is UReferenceExpression) {
val resolved = element.resolve()
if (resolved is PsiVariable) {
// Follow the indirection and inspect the actual definition
UastLintUtils.findLastAssignment(resolved, element)?.let { expression ->
return getJavaClassType(expression)
}
}
if (element is UQualifiedReferenceExpression && element.selector is UCallExpression) {
val call = element.selector as UCallExpression
val name = call.methodName
if (FOR_NAME == name || LOAD_CLASS == name) {
val arguments = call.valueArguments
if (arguments.isNotEmpty()) {
return ConstantEvaluator
.evaluateString(null, arguments[0], false)?.let {
psiFactory!!.createTypeFromText(it, null)
}
}
} else if (GET_CLASS == name) {
return TypeEvaluator.evaluate(element.receiver)
}
// TODO: Are there any other common reflection utility methods (from reflection
// libraries etc) ?
}
}
}
return TypeEvaluator.evaluate(element)
}
private fun reportIssue(
context: JavaContext,
restriction: Restriction?,
api: String,
call: UCallExpression
) {
val targetSdk = context.project.targetSdk
fun fatal() {
context.report(
BLOCKED_PRIVATE_API, call, context.getLocation(call),
"Reflective access to $api is forbidden when targeting API $targetSdk and above"
)
}
fun error() {
context.report(
SOON_BLOCKED_PRIVATE_API, call, context.getLocation(call),
"Reflective access to $api will throw an exception when targeting API $targetSdk and above"
)
}
fun warning() {
context.report(
DISCOURAGED_PRIVATE_API, call, context.getLocation(call),
"Reflective access to $api, which is not part of the public SDK and therefore likely to change in future Android releases"
)
}
when (restriction) {
Restriction.BLACK -> fatal()
Restriction.GREY_MAX_O ->
if (targetSdk <= AndroidVersion.VersionCodes.O ||
VersionChecks.isWithinVersionCheckConditional(
context.evaluator, call, AndroidVersion.VersionCodes.O, false
)
) {
warning()
} else {
error()
}
Restriction.GREY_MAX_P ->
if (targetSdk <= AndroidVersion.VersionCodes.P ||
VersionChecks.isWithinVersionCheckConditional(
context.evaluator, call, AndroidVersion.VersionCodes.P, false
)
) {
warning()
} else {
error()
}
Restriction.GREY -> warning()
else -> return // nothing to report
}
}
}