blob: d12145c33fe8140705a7927162b734b0e746b79d [file] [log] [blame]
/*
* Copyright (C) 2015 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.tools.lint.checks.CutPasteDetector.isReachableFrom
import com.android.tools.lint.detector.api.Category
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.getMethodName
import com.android.tools.lint.detector.api.getUMethod
import com.google.common.collect.Lists
import com.google.common.collect.Maps
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiField
import com.intellij.psi.PsiLocalVariable
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiParameter
import com.intellij.psi.PsiVariable
import org.jetbrains.uast.UAnonymousClass
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.ULocalVariable
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.UVariable
import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.tryResolve
import org.jetbrains.uast.util.isMethodCall
import org.jetbrains.uast.visitor.AbstractUastVisitor
/** Checks related to RecyclerView usage. */
class RecyclerViewDetector : Detector(), SourceCodeScanner {
// ---- implements SourceCodeScanner ----
override fun applicableSuperClasses(): List<String>? {
return listOf(VIEW_ADAPTER)
}
override fun visitClass(context: JavaContext, declaration: UClass) {
val evaluator = context.evaluator
for (method in declaration.findMethodsByName(ON_BIND_VIEW_HOLDER, false)) {
val size = evaluator.getParameterCount(method)
if (size == 2 || size == 3) {
checkMethod(context, method, declaration)
}
}
}
private fun checkMethod(
context: JavaContext,
declaration: PsiMethod,
cls: PsiClass
) {
val parameters = declaration.parameterList.parameters
val viewHolder = parameters[0]
val parameter = parameters[1]
val visitor = ParameterEscapesVisitor(cls, parameter)
val method = declaration.getUMethod() ?: return
method.accept(visitor)
if (visitor.variableEscapes()) {
reportError(context, viewHolder, parameter)
}
// Look for pending data binder calls that aren't executed before the method finishes
checkDataBinders(context, method, visitor.dataBinders)
}
private fun reportError(
context: JavaContext,
viewHolder: PsiParameter,
parameter: PsiParameter
) {
var variablePrefix = viewHolder.name
if (variablePrefix == null) {
variablePrefix = "ViewHolder"
}
val message =
"Do not treat position as fixed; only use immediately " +
"and call `$variablePrefix.getAdapterPosition()` to look it up later"
context.report(FIXED_POSITION, parameter, context.getLocation(parameter), message)
}
private fun checkDataBinders(
context: JavaContext,
declaration: UMethod,
references: List<UCallExpression>?
) {
if (references != null && references.isNotEmpty()) {
val targets = Lists.newArrayList<UCallExpression>()
val sources = Lists.newArrayList<UCallExpression>()
for (ref in references) {
if (isExecutePendingBindingsCall(ref)) {
targets.add(ref)
} else {
sources.add(ref)
}
}
// Only operate on the last call in each block: ignore siblings with the same parent
// That way if you have
// dataBinder.foo();
// dataBinder.bar();
// dataBinder.baz();
// we only flag the *last* of these calls as needing an executePendingBindings
// afterwards. We do this with a parent map such that we correctly pair
// elements when they have nested references within (such as if blocks.)
val parentToChildren = Maps.newHashMap<UElement, UCallExpression>()
for (reference in sources) {
// Note: We're using a map, not a multimap, and iterating forwards:
// this means that the *last* element will overwrite previous entries,
// and we end up with the last reference for each parent which is what we
// want
val statement =
reference.getParentOfType<UExpression>(UExpression::class.java, true)
if (statement != null) {
parentToChildren[statement.uastParent] = reference
}
}
for (source in parentToChildren.values) {
val sourceBinderReference = source.receiver ?: continue
val sourceDataBinder = getDataBinderReference(sourceBinderReference) ?: continue
var reachesTarget = false
for (target in targets) {
if (sourceDataBinder == getDataBinderReference(target.receiver) &&
// TODO: Provide full control flow graph, or at least provide an
// isReachable method which can take multiple targets
isReachableFrom(declaration, source, target)
) {
reachesTarget = true
break
}
}
if (!reachesTarget) {
val lhs = sourceBinderReference.asSourceString()
val message =
"You must call `$lhs.executePendingBindings()` " +
"before the `onBind` method exits, otherwise, the DataBinding " +
"library will update the UI in the next animation frame " +
"causing a delayed update & potential jumps if the item " +
"resizes."
val location = context.getLocation(source)
context.report(DATA_BINDER, source, location, message)
}
}
}
}
private fun isExecutePendingBindingsCall(call: UCallExpression): Boolean {
return "executePendingBindings" == getMethodName(call)
}
/**
* Determines whether a given variable "escapes" either to a field or to a nested runnable. (We
* deliberately ignore variables that escape via method calls.)
*/
private class ParameterEscapesVisitor(
private val bindClass: PsiClass,
variable: PsiParameter
) : AbstractUastVisitor() {
private val variables: MutableList<PsiVariable>
private var escapes: Boolean = false
private var foundInnerClass: Boolean = false
var dataBinders: MutableList<UCallExpression>? = null
init {
variables = Lists.newArrayList(variable)
}
fun variableEscapes(): Boolean {
return escapes
}
override fun visitVariable(node: UVariable): Boolean {
val initializer = node.uastInitializer
if (initializer is UReferenceExpression) {
val resolved = initializer.resolve()
if (resolved != null && variables.contains(resolved)) {
if (resolved is ULocalVariable) {
variables.add(node)
} else if (resolved is PsiField) {
escapes = true
}
}
}
return super.visitVariable(node)
}
override fun visitBinaryExpression(node: UBinaryExpression): Boolean {
if (node.operator is UastBinaryOperator.AssignOperator) {
val rhs = node.rightOperand
var clearLhs = true
if (rhs is UReferenceExpression) {
val resolved = rhs.resolve()
if (resolved != null && variables.contains(resolved)) {
clearLhs = false
val resolvedLhs = node.leftOperand.tryResolve()
if (resolvedLhs is PsiLocalVariable) {
variables.add(resolvedLhs)
} else if (resolvedLhs is PsiField) {
escapes = true
}
}
}
if (clearLhs) {
// If we reassign one of the variables, clear it out
val resolved = node.leftOperand.tryResolve()
if (resolved != null) {
variables.remove(resolved)
}
}
}
return super.visitBinaryExpression(node)
}
override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression): Boolean {
if (foundInnerClass) {
// Check to see if this reference is inside the same class as the original
// onBind (e.g. is this a reference from an inner class, or a reference
// to a variable assigned from there)
val resolved = node.resolve()
if (resolved != null && variables.contains(resolved)) {
val outer = node.getParentOfType<UElement>(UClass::class.java, true)
if (bindClass != outer) {
escapes = true
}
}
}
return super.visitSimpleNameReferenceExpression(node)
}
override fun visitClass(node: UClass): Boolean {
if (node is UAnonymousClass || !node.isStatic) {
foundInnerClass = true
}
return super.visitClass(node)
}
override fun visitCallExpression(node: UCallExpression): Boolean {
if (node.isMethodCall()) {
val methodExpression = node.receiver
val dataBinder = getDataBinderReference(methodExpression)
if (dataBinder != null) {
val list = dataBinders ?: run {
val new = Lists.newArrayList<UCallExpression>()
dataBinders = new
new
}
list.add(node)
}
}
return super.visitCallExpression(node)
}
}
companion object {
private val IMPLEMENTATION =
Implementation(RecyclerViewDetector::class.java, Scope.JAVA_FILE_SCOPE)
@JvmField
val FIXED_POSITION = Issue.create(
id = "RecyclerView",
briefDescription = "RecyclerView Problems",
explanation = """
`RecyclerView` will **not** call `onBindViewHolder` again when the position \
of the item changes in the data set unless the item itself is invalidated or \
the new position cannot be determined.
For this reason, you should **only** use the position parameter while \
acquiring the related data item inside this method, and should **not** keep \
a copy of it.
If you need the position of an item later on (e.g. in a click listener), use \
`getAdapterPosition()` which will have the updated adapter position.
""",
category = Category.CORRECTNESS,
priority = 8,
androidSpecific = true,
severity = Severity.ERROR,
implementation = IMPLEMENTATION
)
@JvmField
val DATA_BINDER = Issue.create(
id = "PendingBindings",
briefDescription = "Missing Pending Bindings",
explanation = """
When using a `ViewDataBinding` in a `onBindViewHolder` method, you **must** \
call `executePendingBindings()` before the method exits; otherwise the data \
binding runtime will update the UI in the next animation frame causing a \
delayed update and potential jumps if the item resizes.
""",
category = Category.CORRECTNESS,
priority = 8,
androidSpecific = true,
severity = Severity.ERROR,
implementation = IMPLEMENTATION
)
private const val VIEW_ADAPTER = "android.support.v7.widget.RecyclerView.Adapter"
private const val ON_BIND_VIEW_HOLDER = "onBindViewHolder"
private fun getDataBinderReference(element: UElement?): PsiField? {
if (element is UReferenceExpression) {
val resolved = element.resolve()
if (resolved is PsiField) {
if ("dataBinder" == resolved.name) {
return resolved
}
}
}
return null
}
}
}