blob: e2cf80a052295fec3d841d17c8c66a648bbf43ae [file] [log] [blame]
/*
* Copyright 2020 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 androidx.activity.lint
import com.android.builder.model.AndroidLibrary
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Detector.UastScanner
import com.android.tools.lint.detector.api.GradleContext
import com.android.tools.lint.detector.api.GradleScanner
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.Location
import com.android.tools.lint.detector.api.Project
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import java.util.EnumSet
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
class ActivityResultFragmentVersionDetector : Detector(), UastScanner, GradleScanner {
companion object {
const val FRAGMENT_VERSION = "1.3.0-beta02"
val ISSUE = Issue.create(
id = "InvalidFragmentVersionForActivityResult",
briefDescription = "Update to Fragment $FRAGMENT_VERSION to use ActivityResult APIs",
explanation = """In order to use the ActivityResult APIs you must upgrade your \
Fragment version to $FRAGMENT_VERSION. Previous versions of FragmentActivity \
failed to call super.onRequestPermissionsResult() and used invalid request codes""",
category = Category.CORRECTNESS,
severity = Severity.FATAL,
implementation = Implementation(
ActivityResultFragmentVersionDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.GRADLE_FILE),
Scope.JAVA_FILE_SCOPE,
Scope.GRADLE_SCOPE
)
).addMoreInfo(
"https://developer.android.com/training/permissions/requesting#make-the-request"
)
}
var locations = ArrayList<Location>()
lateinit var expression: UCallExpression
private var checkedImplementationDependencies = false
override fun getApplicableUastTypes(): List<Class<out UElement>>? {
return listOf(UCallExpression::class.java)
}
override fun createUastHandler(context: JavaContext): UElementHandler? {
return object : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
if (node.methodIdentifier?.name != "registerForActivityResult") {
return
}
expression = node
locations.add(context.getLocation(node))
}
}
}
override fun checkDslPropertyAssignment(
context: GradleContext,
property: String,
value: String,
parent: String,
parentParent: String?,
valueCookie: Any,
statementCookie: Any
) {
if (locations.isEmpty()) {
return
}
if (property == "api") {
// always check api dependencies
reportIssue(value, context)
} else if (!checkedImplementationDependencies) {
val project = context.project
if (useNewLintVersion(project)) {
checkWithNewLintVersion(project, context)
} else {
checkWithOldLintVersion(project, context)
}
}
}
private fun useNewLintVersion(project: Project): Boolean {
project::class.memberFunctions.forEach { function ->
if (function.name == "getBuildVariant") {
return true
}
}
return false
}
private fun checkWithNewLintVersion(project: Project, context: GradleContext) {
val buildVariant = callFunctionWithReflection(project, "getBuildVariant")
val mainArtifact = getMemberWithReflection(buildVariant, "mainArtifact")
val dependencies = getMemberWithReflection(mainArtifact, "dependencies")
val all = callFunctionWithReflection(dependencies, "getAll")
(all as ArrayList<*>).forEach { lmLibrary ->
lmLibrary::class.memberProperties.forEach { libraryMembers ->
if (libraryMembers.name == "resolvedCoordinates") {
reportIssue(libraryMembers.call(lmLibrary).toString(), context, false)
}
}
}
}
private fun checkWithOldLintVersion(project: Project, context: GradleContext) {
lateinit var explicitLibraries: Collection<AndroidLibrary>
val currentVariant = callFunctionWithReflection(project, "getCurrentVariant")
val mainArtifact = callFunctionWithReflection(currentVariant, "getMainArtifact")
val dependencies = callFunctionWithReflection(mainArtifact, "getDependencies")
@Suppress("UNCHECKED_CAST")
explicitLibraries =
callFunctionWithReflection(dependencies, "getLibraries") as Collection<AndroidLibrary>
// collect all of the library dependencies
val allLibraries = HashSet<AndroidLibrary>()
addIndirectAndroidLibraries(explicitLibraries, allLibraries)
// check all of the dependencies
allLibraries.forEach {
val resolvedCoords = it.resolvedCoordinates
val groupId = resolvedCoords.groupId
val artifactId = resolvedCoords.artifactId
val version = resolvedCoords.version
reportIssue("$groupId:$artifactId:$version", context, false)
}
}
private fun callFunctionWithReflection(caller: Any, functionName: String): Any {
caller::class.memberFunctions.forEach { function ->
if (function.name == functionName) {
function.isAccessible = true
return function.call(caller)!!
}
}
return Unit
}
private fun getMemberWithReflection(caller: Any, memberName: String): Any {
caller::class.memberProperties.forEach { member ->
if (member.name == memberName) {
return member.getter.call(caller)!!
}
}
return Unit
}
private fun addIndirectAndroidLibraries(
libraries: Collection<AndroidLibrary>,
result: MutableSet<AndroidLibrary>
) {
for (library in libraries) {
if (!result.contains(library)) {
result.add(library)
addIndirectAndroidLibraries(library.libraryDependencies, result)
}
}
}
private fun reportIssue(
value: String,
context: GradleContext,
removeQuotes: Boolean = true
) {
val library = if (removeQuotes) {
getStringLiteralValue(value)
} else {
value
}
if (library.isNotEmpty() &&
library.substringAfter("androidx.fragment:fragment:") < FRAGMENT_VERSION
) {
locations.forEach { location ->
context.report(
ISSUE, expression, location,
"Upgrade Fragment version to at least $FRAGMENT_VERSION."
)
}
}
}
/**
* Extracts the string value from the DSL value by removing surrounding quotes.
*
* Returns an empty string if [value] is not a string literal.
*/
private fun getStringLiteralValue(value: String): String {
if (value.length > 2 && (
value.startsWith("'") && value.endsWith("'") ||
value.startsWith("\"") && value.endsWith("\"")
)
) {
return value.substring(1, value.length - 1)
}
return ""
}
}