/*
* Copyright (C) 2012 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.klint.checks;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.*;
import com.google.common.collect.Lists;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.util.containers.Predicate;
import org.jetbrains.uast.*;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Arrays;
import java.util.List;
import static com.android.SdkConstants.CLASS_CONTENTPROVIDER;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.tools.klint.detector.api.LintUtils.skipParentheses;
import static org.jetbrains.uast.UastUtils.*;
/**
* Checks for missing {@code recycle} calls on resources that encourage it, and
* for missing {@code commit} calls on FragmentTransactions, etc.
*/
public class CleanupDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
CleanupDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Problems with missing recycle calls */
public static final Issue RECYCLE_RESOURCE = Issue.create(
"Recycle", //$NON-NLS-1$
"Missing `recycle()` calls",
"Many resources, such as TypedArrays, VelocityTrackers, etc., " +
"should be recycled (with a `recycle()` call) after use. This lint check looks " +
"for missing `recycle()` calls.",
Category.PERFORMANCE,
7,
Severity.WARNING,
IMPLEMENTATION);
/** Problems with missing commit calls. */
public static final Issue COMMIT_FRAGMENT = Issue.create(
"CommitTransaction", //$NON-NLS-1$
"Missing `commit()` calls",
"After creating a `FragmentTransaction`, you typically need to commit it as well",
Category.CORRECTNESS,
7,
Severity.WARNING,
IMPLEMENTATION);
/** The main issue discovered by this detector */
public static final Issue SHARED_PREF = Issue.create(
"CommitPrefEdits", //$NON-NLS-1$
"Missing `commit()` on `SharedPreference` editor",
"After calling `edit()` on a `SharedPreference`, you must call `commit()` " +
"or `apply()` on the editor to save the results.",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
CleanupDetector.class,
Scope.JAVA_FILE_SCOPE));
// Target method names
private static final String RECYCLE = "recycle"; //$NON-NLS-1$
private static final String RELEASE = "release"; //$NON-NLS-1$
private static final String OBTAIN = "obtain"; //$NON-NLS-1$
private static final String SHOW = "show"; //$NON-NLS-1$
private static final String ACQUIRE_CPC = "acquireContentProviderClient"; //$NON-NLS-1$
private static final String OBTAIN_NO_HISTORY = "obtainNoHistory"; //$NON-NLS-1$
private static final String OBTAIN_ATTRIBUTES = "obtainAttributes"; //$NON-NLS-1$
private static final String OBTAIN_TYPED_ARRAY = "obtainTypedArray"; //$NON-NLS-1$
private static final String OBTAIN_STYLED_ATTRIBUTES = "obtainStyledAttributes"; //$NON-NLS-1$
private static final String BEGIN_TRANSACTION = "beginTransaction"; //$NON-NLS-1$
private static final String COMMIT = "commit"; //$NON-NLS-1$
private static final String APPLY = "apply"; //$NON-NLS-1$
private static final String COMMIT_ALLOWING_LOSS = "commitAllowingStateLoss"; //$NON-NLS-1$
private static final String QUERY = "query"; //$NON-NLS-1$
private static final String RAW_QUERY = "rawQuery"; //$NON-NLS-1$
private static final String QUERY_WITH_FACTORY = "queryWithFactory"; //$NON-NLS-1$
private static final String RAW_QUERY_WITH_FACTORY = "rawQueryWithFactory"; //$NON-NLS-1$
private static final String CLOSE = "close"; //$NON-NLS-1$
private static final String USE = "use"; //$NON-NLS-1$
private static final String EDIT = "edit"; //$NON-NLS-1$
private static final String MOTION_EVENT_CLS = "android.view.MotionEvent"; //$NON-NLS-1$
private static final String PARCEL_CLS = "android.os.Parcel"; //$NON-NLS-1$
private static final String VELOCITY_TRACKER_CLS = "android.view.VelocityTracker";//$NON-NLS-1$
private static final String DIALOG_FRAGMENT = "android.app.DialogFragment"; //$NON-NLS-1$
private static final String DIALOG_V4_FRAGMENT =
"android.support.v4.app.DialogFragment"; //$NON-NLS-1$
private static final String FRAGMENT_MANAGER_CLS = "android.app.FragmentManager"; //$NON-NLS-1$
private static final String FRAGMENT_MANAGER_V4_CLS =
"android.support.v4.app.FragmentManager"; //$NON-NLS-1$
private static final String FRAGMENT_TRANSACTION_CLS =
"android.app.FragmentTransaction"; //$NON-NLS-1$
private static final String FRAGMENT_TRANSACTION_V4_CLS =
"android.support.v4.app.FragmentTransaction"; //$NON-NLS-1$
public static final String SURFACE_CLS = "android.view.Surface";
public static final String SURFACE_TEXTURE_CLS = "android.graphics.SurfaceTexture";
public static final String CONTENT_PROVIDER_CLIENT_CLS
= "android.content.ContentProviderClient";
public static final String CONTENT_RESOLVER_CLS = "android.content.ContentResolver";
@SuppressWarnings("SpellCheckingInspection")
public static final String SQLITE_DATABASE_CLS = "android.database.sqlite.SQLiteDatabase";
public static final String CURSOR_CLS = "android.database.Cursor";
public static final String ANDROID_CONTENT_SHARED_PREFERENCES =
"android.content.SharedPreferences"; //$NON-NLS-1$
private static final String ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR =
"android.content.SharedPreferences.Editor"; //$NON-NLS-1$
private static final String CLOSABLE = "java.io.Closeable"; //$NON-NLS-1$
/** Constructs a new {@link CleanupDetector} */
public CleanupDetector() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
// FragmentManager commit check
BEGIN_TRANSACTION,
// Recycle check
OBTAIN, OBTAIN_NO_HISTORY,
OBTAIN_STYLED_ATTRIBUTES,
OBTAIN_ATTRIBUTES,
OBTAIN_TYPED_ARRAY,
// Release check
ACQUIRE_CPC,
// Cursor close check
QUERY, RAW_QUERY, QUERY_WITH_FACTORY, RAW_QUERY_WITH_FACTORY,
// SharedPreferences check
EDIT
);
}
@Nullable
@Override
public List<String> getApplicableConstructorTypes() {
return Arrays.asList(SURFACE_TEXTURE_CLS, SURFACE_CLS);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
String name = method.getName();
if (BEGIN_TRANSACTION.equals(name)) {
checkTransactionCommits(context, call, method);
} else if (EDIT.equals(name)) {
checkEditorApplied(context, call, method);
} else {
checkResourceRecycled(context, call, method);
}
}
@Override
public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod constructor) {
PsiClass containingClass = constructor.getContainingClass();
if (containingClass != null) {
String type = containingClass.getQualifiedName();
if (type != null) {
checkRecycled(context, node, type, RELEASE);
}
}
}
private static void checkResourceRecycled(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod method) {
String name = method.getName();
// Recycle detector
PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
return;
}
JavaEvaluator evaluator = context.getEvaluator();
if ((OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) &&
InheritanceUtil.isInheritor(containingClass, false, MOTION_EVENT_CLS)) {
checkRecycled(context, node, MOTION_EVENT_CLS, RECYCLE);
} else if (OBTAIN.equals(name) && InheritanceUtil.isInheritor(containingClass, false, PARCEL_CLS)) {
checkRecycled(context, node, PARCEL_CLS, RECYCLE);
} else if (OBTAIN.equals(name) &&
InheritanceUtil.isInheritor(containingClass, false, VELOCITY_TRACKER_CLS)) {
checkRecycled(context, node, VELOCITY_TRACKER_CLS, RECYCLE);
} else if ((OBTAIN_STYLED_ATTRIBUTES.equals(name)
|| OBTAIN_ATTRIBUTES.equals(name)
|| OBTAIN_TYPED_ARRAY.equals(name)) &&
(InheritanceUtil.isInheritor(containingClass, false, CLASS_CONTEXT) ||
InheritanceUtil.isInheritor(containingClass, false, SdkConstants.CLASS_RESOURCES))) {
PsiType returnType = method.getReturnType();
if (returnType instanceof PsiClassType) {
PsiClass cls = ((PsiClassType)returnType).resolve();
if (cls != null && "android.content.res.TypedArray".equals(cls.getQualifiedName())) {
checkRecycled(context, node, "android.content.res.TypedArray", RECYCLE);
}
}
} else if (ACQUIRE_CPC.equals(name) && InheritanceUtil.isInheritor(containingClass,
false, CONTENT_RESOLVER_CLS)) {
checkRecycled(context, node, CONTENT_PROVIDER_CLIENT_CLS, RELEASE);
} else if ((QUERY.equals(name)
|| RAW_QUERY.equals(name)
|| QUERY_WITH_FACTORY.equals(name)
|| RAW_QUERY_WITH_FACTORY.equals(name))
&& (InheritanceUtil.isInheritor(containingClass, false, SQLITE_DATABASE_CLS) ||
InheritanceUtil.isInheritor(containingClass, false, CONTENT_RESOLVER_CLS) ||
InheritanceUtil.isInheritor(containingClass, false, CLASS_CONTENTPROVIDER) ||
InheritanceUtil.isInheritor(containingClass, false, CONTENT_PROVIDER_CLIENT_CLS))) {
// Other potential cursors-returning methods that should be tracked:
// android.app.DownloadManager#query
// android.content.ContentProviderClient#query
// android.content.ContentResolver#query
// android.database.sqlite.SQLiteQueryBuilder#query
// android.provider.Browser#getAllBookmarks
// android.provider.Browser#getAllVisitedUrls
// android.provider.DocumentsProvider#queryChildDocuments
// android.provider.DocumentsProvider#qqueryDocument
// android.provider.DocumentsProvider#queryRecentDocuments
// android.provider.DocumentsProvider#queryRoots
// android.provider.DocumentsProvider#querySearchDocuments
// android.provider.MediaStore$Images$Media#query
// android.widget.FilterQueryProvider#runQuery
checkClosedOrUsed(context, node, CURSOR_CLS);
}
}
private static void reportRecycleResource(JavaContext context, String recycleType, String recycleName, @NonNull UCallExpression node) {
String className = recycleType.substring(recycleType.lastIndexOf('.') + 1);
String message;
if (RECYCLE.equals(recycleName)) {
message = String.format(
"This `%1$s` should be recycled after use with `#recycle()`", className);
} else {
message = String.format(
"This `%1$s` should be freed up after use with `#%2$s()`", className,
recycleName);
}
UElement locationNode = node.getMethodIdentifier();
if (locationNode == null) {
locationNode = node;
}
Location location = context.getUastLocation(locationNode);
context.report(RECYCLE_RESOURCE, node, location, message);
}
private static void checkClosedOrUsed(@NonNull final JavaContext context, @NonNull UCallExpression node,
@NonNull final String recycleType) {
if (isCleanedUpInChain(node, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isCloseMethodCall(call) || isUseMethodCall(call);
}
})) {
return;
}
PsiVariable boundVariable = getVariableElement(node);
if (boundVariable == null) {
reportRecycleResource(context, recycleType, CLOSE, node);
return;
}
UMethod method = getParentOfType(node, UMethod.class, true);
if (method == null) {
return;
}
FinishVisitor visitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull UCallExpression call) {
if (isUseMethodCall(call) || isCloseMethodCall(call)) {
UExpression receiver = call.getReceiver();
if (receiver instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) receiver).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
return false;
}
};
method.accept(visitor);
if (visitor.isCleanedUp() || visitor.variableEscapes()) {
return;
}
reportRecycleResource(context, recycleType, CLOSE, node);
}
private static boolean isCloseMethodCall(UCallExpression call) {
return isValidCleanupMethodCall(call, CLOSE, CLOSABLE);
}
private static boolean isUseMethodCall(UCallExpression call) {
return USE.equals(call.getMethodName());
}
private static boolean isValidCleanupMethodCall(@NonNull UCallExpression call, @NonNull String methodName, @NonNull String className) {
if (!methodName.equals(call.getMethodName())) {
return false;
}
PsiMethod method = call.resolve();
if (method == null) {
return false;
}
return InheritanceUtil.isInheritor(method.getContainingClass(), false, className);
}
private static boolean isCleanedUpInChain(UExpression expression, Predicate<UCallExpression> isCleanupCallPredicate) {
List<UExpression> chain = getQualifiedChain(getOutermostQualified(expression));
boolean skip = true;
for (UExpression e : chain) {
if (e == expression) {
skip = false;
continue;
}
if (skip) {
continue;
}
if (e instanceof UCallExpression) {
UCallExpression call = (UCallExpression) e;
if (isCleanupCallPredicate.apply(call)) {
return true;
}
}
}
return false;
}
private static void checkRecycled(@NonNull final JavaContext context, @NonNull UCallExpression node,
@NonNull final String recycleType, @NonNull final String recycleName) {
if (isCleanedUpInChain(node, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isValidCleanupMethodCall(call, recycleName, recycleType);
}
})) {
return;
}
PsiVariable boundVariable = getVariableElement(node);
if (boundVariable == null) {
reportRecycleResource(context, recycleType, recycleName, node);
return;
}
UMethod method = getParentOfType(node, UMethod.class, true);
if (method == null) {
return;
}
FinishVisitor visitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull UCallExpression call) {
if (isValidCleanupMethodCall(call, recycleName, recycleType)) {
// Yes, called the right recycle() method; now make sure
// we're calling it on the right variable
UExpression operand = call.getReceiver();
if (operand instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) operand).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
return false;
}
};
method.accept(visitor);
if (visitor.isCleanedUp() || visitor.variableEscapes()) {
return;
}
reportRecycleResource(context, recycleType, recycleName, node);
}
private static void checkTransactionCommits(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod calledMethod) {
if (isBeginTransaction(context, calledMethod)) {
if (isCommittedInChainedCalls(context, node)) {
return;
}
PsiVariable boundVariable = getVariableElement(node, true);
if (boundVariable != null) {
UMethod method = getParentOfType(node, UMethod.class, true);
if (method == null) {
return;
}
FinishVisitor commitVisitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull UCallExpression call) {
if (isTransactionCommitMethodCall(mContext, call)) {
List<UExpression> chain = getQualifiedChain(getOutermostQualified(call));
if (chain.isEmpty()) {
return false;
}
UExpression operand = chain.get(0);
if (operand != null) {
PsiElement resolved = UastUtils.tryResolve(operand);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
} else if (resolved instanceof PsiMethod
&& operand instanceof UCallExpression
&& isCommittedInChainedCalls(mContext,
(UCallExpression) operand)) {
// Check that the target of the committed chains is the
// right variable!
while (operand instanceof UCallExpression) {
operand = ((UCallExpression) operand).getReceiver();
}
if (operand instanceof UReferenceExpression) {
resolved = ((UReferenceExpression) operand).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
}
} else if (isShowFragmentMethodCall(mContext, call)) {
List<UExpression> arguments = call.getValueArguments();
if (arguments.size() == 2) {
UExpression first = arguments.get(0);
PsiElement resolved = UastUtils.tryResolve(first);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
return false;
}
};
method.accept(commitVisitor);
if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) {
return;
}
}
String message = "This transaction should be completed with a `commit()` call";
context.report(COMMIT_FRAGMENT, node, context.getUastNameLocation(node), message);
}
}
private static boolean isCommittedInChainedCalls(@NonNull final JavaContext context,
@NonNull UCallExpression node) {
// Look for chained calls since the FragmentManager methods all return "this"
// to allow constructor chaining, e.g.
// getFragmentManager().beginTransaction().addToBackStack("test")
// .disallowAddToBackStack().hide(mFragment2).setBreadCrumbShortTitle("test")
// .show(mFragment2).setCustomAnimations(0, 0).commit();
return isCleanedUpInChain(node, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isTransactionCommitMethodCall(context, call) || isShowFragmentMethodCall(context, call);
}
});
}
private static boolean isTransactionCommitMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
String methodName = call.getMethodName();
return (COMMIT.equals(methodName) || COMMIT_ALLOWING_LOSS.equals(methodName)) &&
isMethodOnFragmentClass(context, call,
FRAGMENT_TRANSACTION_CLS,
FRAGMENT_TRANSACTION_V4_CLS,
true);
}
private static boolean isShowFragmentMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
String methodName = call.getMethodName();
return SHOW.equals(methodName)
&& isMethodOnFragmentClass(context, call,
DIALOG_FRAGMENT, DIALOG_V4_FRAGMENT, true);
}
private static boolean isMethodOnFragmentClass(
@NonNull JavaContext context,
@NonNull UCallExpression call,
@NonNull String fragmentClass,
@NonNull String v4FragmentClass,
boolean returnForUnresolved) {
PsiMethod method = call.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
return InheritanceUtil.isInheritor(containingClass, false, fragmentClass) ||
InheritanceUtil.isInheritor(containingClass, false, v4FragmentClass);
} else {
// If we *can't* resolve the method call, caller can decide
// whether to consider the method called or not
return returnForUnresolved;
}
}
private static void checkEditorApplied(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod calledMethod) {
if (isSharedEditorCreation(context, calledMethod)) {
PsiVariable boundVariable = getVariableElement(node, true);
if (isEditorCommittedInChainedCalls(context, node)) {
return;
}
if (boundVariable != null) {
UMethod method = getParentOfType(node, UMethod.class, true);
if (method == null) {
return;
}
FinishVisitor commitVisitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull UCallExpression call) {
if (isEditorApplyMethodCall(mContext, call)
|| isEditorCommitMethodCall(mContext, call)) {
List<UExpression> chain = getQualifiedChain(getOutermostQualified(call));
if (chain.isEmpty()) {
return false;
}
UExpression operand = chain.get(0);
if (operand != null) {
PsiElement resolved = UastUtils.tryResolve(operand);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
} else if (resolved instanceof PsiMethod
&& operand instanceof UCallExpression
&& isEditorCommittedInChainedCalls(mContext,
(UCallExpression) operand)) {
// Check that the target of the committed chains is the
// right variable!
while (operand instanceof UCallExpression) {
operand = ((UCallExpression)operand).getReceiver();
}
if (operand instanceof UReferenceExpression) {
resolved = ((UReferenceExpression) operand).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
}
}
return false;
}
};
method.accept(commitVisitor);
if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) {
return;
}
} else if (UastUtils.getParentOfType(node, UReturnExpression.class) != null) {
// Allocation is in a return statement
return;
}
String message = "`SharedPreferences.edit()` without a corresponding `commit()` or "
+ "`apply()` call";
context.report(SHARED_PREF, node, context.getUastLocation(node), message);
}
}
private static boolean isSharedEditorCreation(@NonNull JavaContext context,
@NonNull PsiMethod method) {
String methodName = method.getName();
if (EDIT.equals(methodName)) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
return InheritanceUtil.isInheritor(
containingClass, false, ANDROID_CONTENT_SHARED_PREFERENCES);
}
return false;
}
private static boolean isEditorCommittedInChainedCalls(@NonNull final JavaContext context,
@NonNull UCallExpression node) {
return isCleanedUpInChain(node, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isEditorCommitMethodCall(context, call) || isEditorApplyMethodCall(context, call);
}
});
}
private static boolean isEditorCommitMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
String methodName = call.getMethodName();
if (COMMIT.equals(methodName)) {
PsiMethod method = call.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
if (InheritanceUtil.isInheritor(containingClass, false,
ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR)) {
suggestApplyIfApplicable(context, call);
return true;
}
}
}
return false;
}
private static boolean isEditorApplyMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
String methodName = call.getMethodName();
if (APPLY.equals(methodName)) {
PsiMethod method = call.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
return InheritanceUtil.isInheritor(containingClass, false,
ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR);
}
}
return false;
}
private static void suggestApplyIfApplicable(@NonNull JavaContext context,
@NonNull UCallExpression node) {
if (context.getProject().getMinSdkVersion().getApiLevel() >= 9) {
// See if the return value is read: can only replace commit with
// apply if the return value is not considered
UElement qualifiedNode = node;
UElement parent = skipParentheses(node.getUastParent());
while (parent instanceof UReferenceExpression) {
qualifiedNode = parent;
parent = skipParentheses(parent.getUastParent());
}
boolean returnValueIgnored = true;
if (parent instanceof UCallExpression
|| parent instanceof UVariable
|| parent instanceof UBinaryExpression
|| parent instanceof UUnaryExpression
|| parent instanceof UReturnExpression) {
returnValueIgnored = false;
} else if (parent instanceof UIfExpression) {
UExpression condition = ((UIfExpression) parent).getCondition();
returnValueIgnored = !condition.equals(qualifiedNode);
} else if (parent instanceof UWhileExpression) {
UExpression condition = ((UWhileExpression) parent).getCondition();
returnValueIgnored = !condition.equals(qualifiedNode);
} else if (parent instanceof UDoWhileExpression) {
UExpression condition = ((UDoWhileExpression) parent).getCondition();
returnValueIgnored = !condition.equals(qualifiedNode);
}
if (returnValueIgnored) {
String message = "Consider using `apply()` instead; `commit` writes "
+ "its data to persistent storage immediately, whereas "
+ "`apply` will handle it in the background";
context.report(SHARED_PREF, node, context.getUastLocation(node), message);
}
}
}
/** Returns the variable the expression is assigned to, if any */
@Nullable
public static PsiVariable getVariableElement(@NonNull UCallExpression rhs) {
return getVariableElement(rhs, false);
}
@Nullable
public static PsiVariable getVariableElement(@NonNull UCallExpression rhs,
boolean allowChainedCalls) {
UElement parent = skipParentheses(
UastUtils.getQualifiedParentOrThis(rhs).getUastParent());
// Handle some types of chained calls; e.g. you might have
// var = prefs.edit().put(key,value)
// and here we want to skip past the put call
if (allowChainedCalls) {
while (true) {
if ((parent instanceof UQualifiedReferenceExpression)) {
UElement parentParent = skipParentheses(parent.getUastParent());
if ((parentParent instanceof UQualifiedReferenceExpression)) {
parent = skipParentheses(parentParent.getUastParent());
} else if (parentParent instanceof UVariable
|| parentParent instanceof UBinaryExpression) {
parent = parentParent;
break;
} else {
break;
}
} else {
break;
}
}
}
if (UastExpressionUtils.isAssignment(parent)) {
UBinaryExpression assignment = (UBinaryExpression) parent;
assert assignment != null;
UExpression lhs = assignment.getLeftOperand();
if (lhs instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) lhs).resolve();
if (resolved instanceof PsiVariable && !(resolved instanceof PsiField)) {
// e.g. local variable, parameter - but not a field
return ((PsiVariable) resolved);
}
}
} else if (parent instanceof UVariable && !(parent instanceof UField)) {
return ((UVariable) parent).getPsi();
}
return null;
}
private static boolean isBeginTransaction(@NonNull JavaContext context, @NonNull PsiMethod method) {
String methodName = method.getName();
if (BEGIN_TRANSACTION.equals(methodName)) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
if (InheritanceUtil.isInheritor(containingClass, false, FRAGMENT_MANAGER_CLS)
|| InheritanceUtil.isInheritor(containingClass, false, FRAGMENT_MANAGER_V4_CLS)) {
return true;
}
}
return false;
}
/**
* Visitor which checks whether an operation is "finished"; in the case
* of a FragmentTransaction we're looking for a "commit" call; in the
* case of a TypedArray we're looking for a "recycle", call, in the
* case of a database cursor we're looking for a "close" call, etc.
*/
private abstract static class FinishVisitor extends AbstractUastVisitor {
protected final JavaContext mContext;
protected final List<PsiVariable> mVariables;
private final PsiVariable mOriginalVariableNode;
private boolean mContainsCleanup;
private boolean mEscapes;
public FinishVisitor(JavaContext context, @NonNull PsiVariable variableNode) {
mContext = context;
mOriginalVariableNode = variableNode;
mVariables = Lists.newArrayList(variableNode);
}
public boolean isCleanedUp() {
return mContainsCleanup;
}
public boolean variableEscapes() {
return mEscapes;
}
@Override
public boolean visitElement(UElement node) {
return mContainsCleanup || super.visitElement(node);
}
protected abstract boolean isCleanupCall(@NonNull UCallExpression call);
@Override
public boolean visitCallExpression(UCallExpression node) {
if (node.getKind() == UastCallKind.METHOD_CALL) {
visitMethodCallExpression(node);
}
return super.visitCallExpression(node);
}
private void visitMethodCallExpression(UCallExpression call) {
if (mContainsCleanup) {
return;
}
// Look for escapes
if (!mEscapes) {
for (UExpression expression : call.getValueArguments()) {
if (expression instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) expression).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
boolean wasEscaped = mEscapes;
mEscapes = true;
// Special case: MotionEvent.obtain(MotionEvent): passing in an
// event here does not recycle the event, and we also know it
// doesn't escape
if (OBTAIN.equals(call.getMethodName())) {
PsiMethod method = call.resolve();
if (JavaEvaluator.isMemberInClass(method, MOTION_EVENT_CLS)) {
mEscapes = wasEscaped;
}
}
}
}
}
}
if (isCleanupCall(call)) {
mContainsCleanup = true;
}
}
@Override
public boolean visitVariable(UVariable variable) {
if (variable instanceof ULocalVariable) {
UExpression initializer = variable.getUastInitializer();
if (initializer instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) initializer).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
mVariables.add(variable.getPsi());
}
}
}
return super.visitVariable(variable);
}
@Override
public boolean visitBinaryExpression(UBinaryExpression expression) {
if (!UastExpressionUtils.isAssignment(expression)) {
return super.visitBinaryExpression(expression);
}
// TEMPORARILY DISABLED; see testDatabaseCursorReassignment
// This can result in some false positives right now. Play it
// safe instead.
boolean clearLhs = false;
UExpression rhs = expression.getRightOperand();
if (rhs instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) rhs).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
clearLhs = false;
PsiElement lhs = UastUtils.tryResolve(expression.getLeftOperand());
if (lhs instanceof PsiLocalVariable) {
mVariables.add(((PsiLocalVariable) lhs));
} else if (lhs instanceof PsiField) {
mEscapes = true;
}
}
}
//noinspection ConstantConditions
if (clearLhs) {
// If we reassign one of the variables, clear it out
PsiElement lhs = UastUtils.tryResolve(expression.getLeftOperand());
//noinspection SuspiciousMethodCalls
if (lhs != null && !lhs.equals(mOriginalVariableNode)
&& mVariables.contains(lhs)) {
//noinspection SuspiciousMethodCalls
mVariables.remove(lhs);
}
}
return super.visitBinaryExpression(expression);
}
@Override
public boolean visitReturnExpression(UReturnExpression node) {
UExpression returnValue = node.getReturnExpression();
if (returnValue instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) returnValue).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
mEscapes = true;
}
}
return super.visitReturnExpression(node);
}
}
}