blob: 686270d6b608deeba840b6fe58d62201a6e7106a [file] [log] [blame]
/*
* Copyright (C) 2018 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.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.isKotlin
import com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UBinaryExpressionWithType
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UPolyadicExpression
import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.visitor.AbstractUastVisitor
/** Checks related to DiffUtil computation. */
class DiffUtilDetector : Detector(), SourceCodeScanner {
// ---- implements SourceCodeScanner ----
override fun applicableSuperClasses(): List<String> {
return listOf(
"android.support.v7.util.DiffUtil.ItemCallback",
"androidx.recyclerview.widget.DiffUtil.ItemCallback",
"android.support.v17.leanback.widget.DiffCallback",
"androidx.leanback.widget.DiffCallback"
)
}
override fun visitClass(context: JavaContext, declaration: UClass) {
val evaluator = context.evaluator
for (method in declaration.methods) {
if (method.name == "areContentsTheSame" && evaluator.getParameterCount(method) == 2) {
checkMethod(context, method)
}
}
}
private fun checkMethod(
context: JavaContext,
declaration: UMethod
) {
declaration.accept(object : AbstractUastVisitor() {
override fun visitBinaryExpression(node: UBinaryExpression): Boolean {
checkExpression(context, node)
return super.visitBinaryExpression(node)
}
override fun visitCallExpression(node: UCallExpression): Boolean {
checkCall(context, node)
return super.visitCallExpression(node)
}
})
}
private fun defaultEquals(context: JavaContext, node: UElement): Boolean {
val resolved: PsiMethod?
if (node is UBinaryExpression) {
resolved = node.resolveOperator()
if (resolved == null) {
val left = node.leftOperand.getExpressionType() as? PsiClassType
return defaultEquals(context, left)
}
} else if (node is UCallExpression) {
resolved = node.resolve()
} else {
// We don't know any better
return false
}
resolved ?: return false
return resolved.containingClass?.qualifiedName == JAVA_LANG_OBJECT
}
private fun defaultEquals(
context: JavaContext,
type: PsiClassType?
): Boolean {
val cls = type?.resolve() ?: return false
if (isKotlin(cls) && (context.evaluator.isSealed(cls) || context.evaluator.isData(cls))) {
// Sealed class doesn't guarantee that it defines equals/hashCode
// but it's likely (we'd need to go look at each inner class)
return false
}
for (m in cls.findMethodsByName("equals", true)) {
if (m is PsiMethod) {
val parameters = m.parameterList.parameters
if (parameters.size == 1 &&
parameters[0].type.canonicalText == JAVA_LANG_OBJECT
) {
return m.containingClass?.qualifiedName == JAVA_LANG_OBJECT
}
}
}
return false
}
private fun checkCall(context: JavaContext, node: UCallExpression) {
if (defaultEquals(context, node)) {
// Within cast or instanceof check which implies a more specific type
// which provides an equals implementation?
if (withinCastWithEquals(context, node)) {
return
}
val targetType = node.receiverType?.canonicalText ?: "target"
val message = "Suspicious equality check: `equals()` is not implemented in $targetType"
val location = context.getCallLocation(node, false, true)
context.report(ISSUE, node, location, message)
}
}
/**
* Is this .equals() call within another if check which checks
* instanceof on a more specific type than we're calling equals on?
* If so, does that more specific type define its own equals?
*/
private fun withinCastWithEquals(context: JavaContext, node: UCallExpression): Boolean {
val ifStatement = node.getParentOfType<UElement>(UIfExpression::class.java, false, UMethod::class.java)
as? UIfExpression ?: return false
val condition = ifStatement.condition
return isCastWithEquals(context, condition)
}
private fun isCastWithEquals(context: JavaContext, node: UExpression): Boolean {
if (node is UBinaryExpressionWithType) {
return !defaultEquals(context, node.type as? PsiClassType)
} else if (node is UPolyadicExpression) {
for (operand in node.operands) {
// Technically we should require && here as well as check that
// the operands being compared is our instance in the if expression
if (isCastWithEquals(context, operand)) {
return true
}
}
}
return false
}
private fun checkExpression(context: JavaContext, node: UBinaryExpression) {
if (node.operator == UastBinaryOperator.IDENTITY_EQUALS ||
node.operator == UastBinaryOperator.EQUALS
) {
val left = node.leftOperand.getExpressionType() ?: return
val right = node.rightOperand.getExpressionType() ?: return
if (left is PsiClassType && right is PsiClassType) {
if (node.operator == UastBinaryOperator.EQUALS) {
if (defaultEquals(context, node)) {
val message =
"Suspicious equality check: `equals()` is not implemented in ${left.className}"
val location = node.operatorIdentifier?.let {
context.getLocation(it)
} ?: context.getLocation(node)
context.report(ISSUE, node, location, message)
}
} else {
val message = if (isKotlin(node.sourcePsi))
"Suspicious equality check: Did you mean `==` instead of `===` ?"
else
"Suspicious equality check: Did you mean `.equals()` instead of `==` ?"
val location = node.operatorIdentifier?.let {
context.getLocation(it)
} ?: context.getLocation(node)
context.report(ISSUE, node, location, message)
}
}
}
}
companion object {
private val IMPLEMENTATION =
Implementation(DiffUtilDetector::class.java, Scope.JAVA_FILE_SCOPE)
@JvmField
val ISSUE = Issue.create(
id = "DiffUtilEquals",
briefDescription = "Suspicious DiffUtil Equality",
explanation = """
`areContentsTheSame` is used by `DiffUtil` to produce diffs. If the \
method is implemented incorrectly, such as using identity equals \
instead of equals, or calling equals on a class that has not implemented \
it, weird visual artifacts can occur.
""",
category = Category.CORRECTNESS,
priority = 4,
androidSpecific = true,
moreInfo = "https://issuetracker.google.com/116789824",
severity = Severity.ERROR,
implementation = IMPLEMENTATION
)
}
}