| /* |
| * 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 |
| } |
| } |
| } |