blob: ee80d7c4540deb50c7d5d03090b774529077e358 [file] [log] [blame]
/*
* Copyright 2019 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.lifecycle.lint
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.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.UastLintUtils
import com.android.tools.lint.detector.api.isKotlin
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiTypeParameter
import com.intellij.psi.PsiVariable
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.PsiImmediateClassType
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtNullableType
import org.jetbrains.kotlin.psi.KtTypeReference
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UField
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getUastParentOfType
import org.jetbrains.uast.isNullLiteral
import org.jetbrains.uast.resolveToUElement
import org.jetbrains.uast.toUElement
/**
* Lint check for ensuring that [androidx.lifecycle.MutableLiveData] values are never null when
* the type is defined as non-nullable in Kotlin.
*/
class NonNullableMutableLiveDataDetector : Detector(), UastScanner {
companion object {
val ISSUE = Issue.Companion.create(
id = "NullSafeMutableLiveData",
briefDescription = "LiveData value assignment nullability mismatch",
explanation = """This check ensures that LiveData values are not null when explicitly \
declared as non-nullable.
Kotlin interoperability does not support enforcing explicit null-safety when using \
generic Java type parameters. Since LiveData is a Java class its value can always \
be null even when its type is explicitly declared as non-nullable. This can lead \
to runtime exceptions from reading a null LiveData value that is assumed to be \
non-nullable.""",
category = Category.INTEROPERABILITY_KOTLIN,
severity = Severity.FATAL,
implementation = Implementation(
NonNullableMutableLiveDataDetector::class.java,
Scope.JAVA_FILE_SCOPE
),
androidSpecific = true
)
}
val typesMap = HashMap<String, KtTypeReference>()
val methods = listOf("setValue", "postValue")
override fun getApplicableUastTypes(): List<Class<out UElement>> {
return listOf(UCallExpression::class.java, UField::class.java)
}
override fun createUastHandler(context: JavaContext): UElementHandler {
return object : UElementHandler() {
override fun visitField(node: UField) {
if (!isKotlin(node.lang)) return
getFieldTypeReference(node)?.let {
// map the variable name to the type reference of its expression.
typesMap.put(node.name, it)
}
}
private fun getFieldTypeReference(element: UField): KtTypeReference? {
// If field has type reference, we need to use type reference
// Given the field `val liveDataField: MutableLiveData<Boolean> = MutableLiveData()`
// reference: `MutableLiveData<Boolean>`
// argument: `Boolean`
val typeReference = element.sourcePsi
?.children
?.firstOrNull { it is KtTypeReference } as? KtTypeReference
val typeArgument = typeReference?.typeElement?.typeArgumentsAsTypes?.singleOrNull()
if (typeArgument != null) {
return typeArgument
}
// We need to extract type from the call expression
// Given the field `val liveDataField = MutableLiveData<Boolean>()`
// expression: `MutableLiveData<Boolean>()`
// argument: `Boolean`
val expression = element.sourcePsi
?.children
?.firstOrNull { it is KtCallExpression } as? KtCallExpression
return expression?.typeArguments?.singleOrNull()?.typeReference
}
override fun visitCallExpression(node: UCallExpression) {
if (!isKotlin(node.lang) || !methods.contains(node.methodName) ||
!context.evaluator.isMemberInSubClassOf(
node.resolve()!!, "androidx.lifecycle.LiveData", false
)
) return
val receiverType = node.receiverType as? PsiClassType
var liveDataType =
if (receiverType != null && receiverType.hasParameters()) {
val receiver =
(node.receiver as? USimpleNameReferenceExpression)?.resolve()
val variable = (receiver as? PsiVariable)
val assignment = variable?.let {
UastLintUtils.findLastAssignment(it, node)
}
val constructorExpression = assignment?.sourcePsi as? KtCallExpression
constructorExpression?.typeArguments?.singleOrNull()?.typeReference
} else {
getTypeArg(receiverType)
}
if (liveDataType == null) {
liveDataType = typesMap[getVariableName(node)] ?: return
}
checkNullability(liveDataType, context, node)
}
private fun getVariableName(node: UCallExpression): String? {
// We need to get the variable this expression is being assigned to
// Given the assignment `liveDataField.value = null`
// node.sourcePsi : `value`
// dot: `.`
// variable: `liveDataField`
val dot = generateSequence(node.sourcePsi?.prevSibling) {
it.prevSibling
}.firstOrNull { it !is PsiWhiteSpace }
val variable = generateSequence(generateSequence(dot?.prevSibling) {
it.prevSibling
}.firstOrNull { it !is PsiWhiteSpace }) {
it.firstChild
}.firstOrNull { it !is PsiWhiteSpace }
return variable?.text
}
}
}
/**
* Iterates [classType]'s hierarchy to find its [androidx.lifecycle.LiveData] value type.
*
* @param classType The [PsiClassType] to search
* @return The LiveData type argument.
*/
fun getTypeArg(classType: PsiClassType?): KtTypeReference? {
if (classType == null) {
return null
}
val cls = classType.resolve().getUastParentOfType<UClass>()
val parentPsiType = cls?.superClassType as PsiClassType
if (parentPsiType.hasParameters()) {
val parentTypeReference = cls.uastSuperTypes[0]
val superType = (parentTypeReference.sourcePsi as KtTypeReference).typeElement
return superType!!.typeArgumentsAsTypes[0]
}
return getTypeArg(parentPsiType)
}
fun checkNullability(
liveDataType: KtTypeReference,
context: JavaContext,
node: UCallExpression
) {
// ignore generic types
if (node.isGenericTypeDefinition()) return
if (liveDataType.typeElement !is KtNullableType) {
val fixes = mutableListOf<LintFix>()
if (context.getLocation(liveDataType).file == context.file) {
// Quick Fixes can only be applied to current file
fixes.add(
fix().name("Change `LiveData` type to nullable")
.replace().with("?").range(context.getLocation(liveDataType)).end().build()
)
}
val argument = node.valueArguments[0]
if (argument.isNullLiteral()) {
// Don't report null!! quick fix.
checkNullability(
context,
argument,
"Cannot set non-nullable LiveData value to `null`",
fixes
)
} else if (argument.isNullable(context)) {
fixes.add(
fix().name("Add non-null asserted (!!) call")
.replace().with("!!").range(context.getLocation(argument)).end().build()
)
checkNullability(context, argument, "Expected non-nullable value", fixes)
}
}
}
private fun UCallExpression.isGenericTypeDefinition(): Boolean {
val classType = typeArguments.singleOrNull() as? PsiImmediateClassType
val resolveGenerics = classType?.resolveGenerics()
return resolveGenerics?.element is PsiTypeParameter
}
/**
* Reports a lint error at [element]'s location with message and quick fixes.
*
* @param context The lint detector context.
* @param element The [UElement] to report this error at.
* @param message The error message to report.
* @param fixes The Lint Fixes to report.
*/
private fun checkNullability(
context: JavaContext,
element: UElement,
message: String,
fixes: List<LintFix>
) {
if (fixes.isEmpty()) {
context.report(ISSUE, context.getLocation(element), message)
} else {
context.report(
ISSUE, context.getLocation(element), message,
fix().alternatives(*fixes.toTypedArray())
)
}
}
}
/**
* Checks if the [UElement] is nullable. Always returns `false` if the [UElement] is not a
* [UReferenceExpression] or [UCallExpression].
*
* @return `true` if instance is nullable, `false` otherwise.
*/
internal fun UElement.isNullable(context: JavaContext): Boolean {
if (this is UCallExpression) {
val psiMethod = resolve() ?: return false
val sourceMethod = psiMethod.toUElement()?.sourcePsi
if (sourceMethod is KtCallableDeclaration) {
// if we have source, check the suspend return type
return sourceMethod.typeReference?.typeElement is KtNullableType
}
// Suspend functions have @Nullable Object return type in JVM
val isSuspendMethod = !context.evaluator.isSuspend(psiMethod)
return psiMethod.hasAnnotation(NULLABLE_ANNOTATION) && isSuspendMethod
} else if (this is UReferenceExpression) {
return (resolveToUElement() as? UAnnotated)?.findAnnotation(NULLABLE_ANNOTATION) != null
}
return false
}
const val NULLABLE_ANNOTATION = "org.jetbrains.annotations.Nullable"