blob: a4c200f6f319cc815b87c6c2774d5e584e2a7983 [file] [log] [blame]
/*
* Copyright (C) 2016 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.SdkConstants.CLASS_APPLICATION
import com.android.SdkConstants.CLASS_CONTEXT
import com.android.SdkConstants.CLASS_FRAGMENT
import com.android.SdkConstants.CLASS_VIEW
import com.android.tools.lint.client.api.JavaEvaluator
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.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.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.intellij.openapi.util.Ref
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
import com.intellij.psi.PsiKeyword
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiModifierList
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.UField
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UObjectLiteralExpression
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.UResolvable
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.util.isAssignment
import org.jetbrains.uast.visitor.AbstractUastVisitor
import java.util.Locale
/** Looks for leaks via static fields */
class LeakDetector : Detector(), SourceCodeScanner {
override fun applicableSuperClasses(): List<String>? {
return SUPER_CLASSES
}
/** Warn about inner classes that aren't static: these end up retaining the outer class */
override fun visitClass(context: JavaContext, declaration: UClass) {
val containingClass = declaration.getContainingUClass()
val isAnonymous = declaration is UAnonymousClass
// Only consider static inner classes
val evaluator = context.evaluator
val isStatic = evaluator.isStatic(declaration) || containingClass == null
if (isStatic || isAnonymous) { // containingClass == null: implicitly static
// But look for fields that store contexts
for (field in declaration.fields) {
checkInstanceField(context, field)
}
if (!isAnonymous) {
return
}
}
var superClass: String? = null
for (cls in SUPER_CLASSES) {
if (evaluator.inheritsFrom(declaration, cls, false)) {
superClass = cls
break
}
}
superClass ?: return
val uastParent = declaration.uastParent
if (uastParent != null) {
val method = uastParent.getParentOfType<UMethod>(
UMethod::class.java,
true,
UClass::class.java,
UObjectLiteralExpression::class.java
)
if (method != null && evaluator.isStatic(method)) {
return
}
}
val invocation = declaration.getParentOfType<UCallExpression>(
UObjectLiteralExpression::class.java,
true,
UMethod::class.java
)
val location: Location
location = if (isAnonymous && invocation != null) {
context.getCallLocation(invocation, false, false)
} else {
context.getNameLocation(declaration)
}
var name: String?
if (isAnonymous) {
name = "anonymous " + (declaration as UAnonymousClass)
.baseClassReference
.qualifiedName
} else {
name = declaration.qualifiedName
if (name == null) {
name = declaration.name
}
}
val superClassName = superClass.substring(superClass.lastIndexOf('.') + 1)
context.report(
ISSUE,
declaration,
location,
"This `$superClassName` class should be static or leaks might occur ($name)"
)
}
override fun getApplicableUastTypes(): List<Class<out UElement>>? {
return listOf<Class<out UElement>>(UField::class.java)
}
override fun createUastHandler(context: JavaContext): UElementHandler? {
return FieldChecker(context)
}
private class FieldChecker(private val context: JavaContext) : UElementHandler() {
override fun visitField(node: UField) {
val modifierList = node.modifierList
if (modifierList == null || !modifierList.hasModifierProperty(PsiModifier.STATIC)) {
return
}
val type = node.type as? PsiClassType ?: return
val fqn = type.canonicalText
if (fqn.startsWith("java.")) {
return
}
val cls = type.resolve() ?: return
if (fqn.startsWith("android.")) {
if (isLeakCandidate(cls, context.evaluator) &&
!isAppContextName(cls, node) &&
!isInitializedToAppContext(node, cls)
) {
val message =
"Do not place Android context classes in static fields; " +
"this is a memory leak"
report(node, modifierList, message)
}
} else {
// User application object -- look to see if that one itself has
// static fields?
// We only check *one* level of indirection here
for ((count, referenced) in cls.allFields.withIndex()) {
// Only check a few; avoid getting bogged down on large classes
if (count == 20) {
break
}
val innerType = referenced.type as? PsiClassType ?: continue
val canonical = innerType.canonicalText
if (canonical.startsWith("java.")) {
continue
}
val innerCls = innerType.resolve() ?: continue
if (canonical.startsWith("android.")) {
if (isLeakCandidate(innerCls, context.evaluator) &&
!isAppContextName(innerCls, referenced) &&
!isInitializedToAppContext(context, referenced, innerCls)
) {
val message = "Do not place Android context classes in static " +
"fields (static reference to `${cls.name}` which has field " +
"`${referenced.name}` pointing to `${innerCls.name}`); this " +
"is a memory leak"
report(node, modifierList, message)
break
}
}
}
}
}
private fun isInitializedToAppContext(
context: JavaContext,
field: PsiField,
typeClass: PsiClass
): Boolean {
if (!context.evaluator.extendsClass(typeClass, CLASS_CONTEXT, false)) {
return false
}
val uField = field.toUElement(UField::class.java) ?: return true
return isInitializedToAppContext(uField, typeClass)
}
/**
* If it's a static field see if it's initialized to an app context in one of the
* constructors
*/
private fun isInitializedToAppContext(field: UField, typeClass: PsiClass): Boolean {
val containingClass = field.getContainingUClass() ?: return false
// Only check for app context if we're dealing with a Context field -- there's
// no chance Fragments, Views etc will be the app context.
if (!context.evaluator.extendsClass(typeClass, CLASS_CONTEXT, false)) {
return false
}
for (method in containingClass.uastDeclarations) {
if (method !is UMethod || !method.isConstructor) {
continue
}
val methodBody = method.uastBody ?: continue
val assignedToAppContext = Ref(false)
methodBody.accept(
object : AbstractUastVisitor() {
override fun visitBinaryExpression(node: UBinaryExpression): Boolean {
if (node.isAssignment() &&
node.leftOperand is UResolvable &&
field.sourcePsi == (node.leftOperand as UResolvable)
.resolve()
) {
// Yes, assigning to this field
// See if the right hand side looks like an app context
var rhs: UElement = node.rightOperand
while (rhs is UQualifiedReferenceExpression) {
rhs = rhs.selector
}
if (rhs is UCallExpression) {
if ("getApplicationContext" == getMethodName(rhs)) {
assignedToAppContext.set(true)
}
}
}
return super.visitBinaryExpression(node)
}
})
if (assignedToAppContext.get()) {
return true
}
}
return false
}
private fun report(
field: PsiField,
modifierList: PsiModifierList,
message: String
) {
var locationNode: PsiElement = field
// Try to find the static modifier itself
if (modifierList.hasExplicitModifier(PsiModifier.STATIC)) {
var child: PsiElement? = modifierList.firstChild
while (child != null) {
if (child is PsiKeyword && PsiKeyword.STATIC == child.text) {
locationNode = child
break
}
child = child.nextSibling
}
}
val location = context.getLocation(locationNode)
context.report(ISSUE, field, location, message)
}
}
private fun checkInstanceField(context: JavaContext, field: UField) {
val type = field.type as? PsiClassType ?: return
val fqn = type.canonicalText
if (fqn.startsWith("java.")) {
return
}
val cls = type.resolve() ?: return
if (isLeakCandidate(cls, context.evaluator)) {
context.report(
LeakDetector.ISSUE,
field,
context.getLocation(field),
"This field leaks a context object"
)
}
}
companion object {
/** Leaking data via static fields */
@JvmField
val ISSUE = Issue.create(
id = "StaticFieldLeak",
briefDescription = "Static Field Leaks",
explanation = """
A static field will leak contexts.
Non-static inner classes have an implicit reference to their outer class. \
If that outer class is for example a `Fragment` or `Activity`, then this \
reference means that the long-running handler/loader/task will hold a \
reference to the activity which prevents it from getting garbage collected.
Similarly, direct field references to activities and fragments from these \
longer running instances can cause leaks.
ViewModel classes should never point to Views or non-application Contexts.
""",
category = Category.PERFORMANCE,
androidSpecific = true,
priority = 6,
severity = Severity.WARNING,
implementation = Implementation(LeakDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
private val SUPER_CLASSES = listOf(
"android.content.Loader",
"android.support.v4.content.Loader",
"android.os.AsyncTask",
"android.arch.lifecycle.ViewModel",
"androidx.lifecycle.ViewModel"
)
private const val CLASS_LIFECYCLE = "androidx.lifecycle.Lifecycle"
private const val CLASS_LIFECYCLE_OLD = "android.arch.lifecycle.Lifecycle"
private fun isAppContextName(cls: PsiClass, field: PsiField): Boolean {
// Don't flag names like "sAppContext" or "applicationContext".
val name = field.name
val lower = name.toLowerCase(Locale.US)
if (lower.contains("appcontext") || lower.contains("application")) {
if (CLASS_CONTEXT == cls.qualifiedName) {
return true
}
}
return false
}
private fun isLeakCandidate(cls: PsiClass, evaluator: JavaEvaluator): Boolean {
return (evaluator.extendsClass(cls, CLASS_CONTEXT, false) &&
!evaluator.extendsClass(cls, CLASS_APPLICATION, false)) ||
evaluator.extendsClass(cls, CLASS_VIEW, false) ||
evaluator.extendsClass(cls, CLASS_FRAGMENT, false) ||
// From https://developer.android.com/topic/libraries/architecture/viewmodel:
// Caution: A ViewModel must never reference a view, Lifecycle, or any
// class that may hold a reference to the activity context
evaluator.extendsClass(cls, CLASS_LIFECYCLE, false) ||
evaluator.extendsClass(cls, CLASS_LIFECYCLE_OLD, false)
}
}
}