| /* |
| * 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.lint.checks; |
| |
| import static com.android.SdkConstants.CLASS_CONTEXT; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedClass; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedField; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedMethod; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedNode; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedVariable; |
| import com.android.tools.lint.client.api.JavaParser.TypeDescriptor; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Detector; |
| import com.android.tools.lint.detector.api.Detector.JavaScanner; |
| 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.google.common.collect.Lists; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| |
| import lombok.ast.AstVisitor; |
| import lombok.ast.BinaryExpression; |
| import lombok.ast.BinaryOperator; |
| import lombok.ast.ConstructorInvocation; |
| import lombok.ast.Expression; |
| import lombok.ast.ForwardingAstVisitor; |
| import lombok.ast.MethodInvocation; |
| import lombok.ast.Node; |
| import lombok.ast.Return; |
| import lombok.ast.StrictListAccessor; |
| import lombok.ast.VariableDefinitionEntry; |
| import lombok.ast.VariableReference; |
| |
| /** |
| * 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 JavaScanner { |
| |
| 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); |
| |
| // 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 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 MOTION_EVENT_CLS = "android.view.MotionEvent"; //$NON-NLS-1$ |
| private static final String RESOURCES_CLS = "android.content.res.Resources"; //$NON-NLS-1$ |
| private static final String PARCEL_CLS = "android.os.Parcel"; //$NON-NLS-1$ |
| private static final String TYPED_ARRAY_CLS = "android.content.res.TypedArray"; //$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"; |
| public static final String CONTENT_PROVIDER_CLS = "android.content.ContentProvider"; |
| @SuppressWarnings("SpellCheckingInspection") |
| public static final String SQLITE_DATABASE_CLS = "android.database.sqlite.SQLiteDatabase"; |
| public static final String CURSOR_CLS = "android.database.Cursor"; |
| |
| /** Constructs a new {@link CleanupDetector} */ |
| public CleanupDetector() { |
| } |
| |
| // ---- Implements JavaScanner ---- |
| |
| @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 |
| ); |
| } |
| |
| @Nullable |
| @Override |
| public List<String> getApplicableConstructorTypes() { |
| return Arrays.asList(SURFACE_TEXTURE_CLS, SURFACE_CLS); |
| } |
| |
| @Override |
| public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, |
| @NonNull MethodInvocation node) { |
| |
| String name = node.astName().astValue(); |
| if (BEGIN_TRANSACTION.equals(name)) { |
| checkTransactionCommits(context, node); |
| } else { |
| checkResourceRecycled(context, node, name); |
| } |
| } |
| |
| @Override |
| public void visitConstructor(@NonNull JavaContext context, @Nullable AstVisitor visitor, |
| @NonNull ConstructorInvocation node, @NonNull ResolvedMethod constructor) { |
| checkRecycled(context, node, constructor.getContainingClass().getSignature(), RELEASE); |
| } |
| |
| private static void checkResourceRecycled(@NonNull JavaContext context, |
| @NonNull MethodInvocation node, @NonNull String name) { |
| // Recycle detector |
| ResolvedNode resolved = context.resolve(node); |
| if (!(resolved instanceof ResolvedMethod)) { |
| return; |
| } |
| ResolvedMethod method = (ResolvedMethod) resolved; |
| ResolvedClass containingClass = method.getContainingClass(); |
| if ((OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) && |
| containingClass.isSubclassOf(MOTION_EVENT_CLS, false)) { |
| checkRecycled(context, node, MOTION_EVENT_CLS, RECYCLE); |
| } else if (OBTAIN.equals(name) && containingClass.isSubclassOf(PARCEL_CLS, false)) { |
| checkRecycled(context, node, PARCEL_CLS, RECYCLE); |
| } else if (OBTAIN.equals(name) && |
| containingClass.isSubclassOf(VELOCITY_TRACKER_CLS, false)) { |
| checkRecycled(context, node, VELOCITY_TRACKER_CLS, RECYCLE); |
| } else if ((OBTAIN_STYLED_ATTRIBUTES.equals(name) |
| || OBTAIN_ATTRIBUTES.equals(name) |
| || OBTAIN_TYPED_ARRAY.equals(name)) && |
| (containingClass.isSubclassOf(CLASS_CONTEXT, false) || |
| containingClass.isSubclassOf(RESOURCES_CLS, false))) { |
| TypeDescriptor returnType = method.getReturnType(); |
| if (returnType != null && returnType.matchesSignature(TYPED_ARRAY_CLS)) { |
| checkRecycled(context, node, TYPED_ARRAY_CLS, RECYCLE); |
| } |
| } else if (ACQUIRE_CPC.equals(name) && containingClass.isSubclassOf( |
| CONTENT_RESOLVER_CLS, false)) { |
| 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)) |
| && (containingClass.isSubclassOf(SQLITE_DATABASE_CLS, false) || |
| containingClass.isSubclassOf(CONTENT_RESOLVER_CLS, false) || |
| containingClass.isSubclassOf(CONTENT_PROVIDER_CLS, false) || |
| containingClass.isSubclassOf(CONTENT_PROVIDER_CLIENT_CLS, false))) { |
| // 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 |
| checkRecycled(context, node, CURSOR_CLS, CLOSE); |
| } |
| } |
| |
| private static void checkRecycled(@NonNull final JavaContext context, @NonNull Node node, |
| @NonNull final String recycleType, @NonNull final String recycleName) { |
| ResolvedVariable boundVariable = getVariable(context, node); |
| if (boundVariable == null) { |
| return; |
| } |
| |
| Node method = JavaContext.findSurroundingMethod(node); |
| if (method == null) { |
| return; |
| } |
| |
| FinishVisitor visitor = new FinishVisitor(context, boundVariable) { |
| @Override |
| protected boolean isCleanupCall(@NonNull MethodInvocation call) { |
| String methodName = call.astName().astValue(); |
| if (!recycleName.equals(methodName)) { |
| return false; |
| } |
| ResolvedNode resolved = mContext.resolve(call); |
| if (resolved instanceof ResolvedMethod) { |
| ResolvedClass containingClass = ((ResolvedMethod) resolved).getContainingClass(); |
| if (containingClass.isSubclassOf(recycleType, false)) { |
| // Yes, called the right recycle() method; now make sure |
| // we're calling it on the right variable |
| Expression operand = call.astOperand(); |
| if (operand != null) { |
| resolved = mContext.resolve(operand); |
| //noinspection SuspiciousMethodCalls |
| if (resolved != null && mVariables.contains(resolved)) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| }; |
| |
| method.accept(visitor); |
| if (visitor.isCleanedUp() || visitor.variableEscapes()) { |
| return; |
| } |
| |
| 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); |
| } |
| Node locationNode = node instanceof MethodInvocation ? |
| ((MethodInvocation) node).astName() : node; |
| Location location = context.getLocation(locationNode); |
| context.report(RECYCLE_RESOURCE, node, location, message); |
| } |
| |
| private static boolean checkTransactionCommits(@NonNull JavaContext context, |
| @NonNull MethodInvocation node) { |
| if (isBeginTransaction(context, node)) { |
| ResolvedVariable boundVariable = getVariable(context, node); |
| if (boundVariable == null && isCommittedInChainedCalls(context, node)) { |
| return true; |
| } |
| |
| if (boundVariable != null) { |
| Node method = JavaContext.findSurroundingMethod(node); |
| if (method == null) { |
| return true; |
| } |
| |
| FinishVisitor commitVisitor = new FinishVisitor(context, boundVariable) { |
| @Override |
| protected boolean isCleanupCall(@NonNull MethodInvocation call) { |
| if (isTransactionCommitMethodCall(mContext, call)) { |
| Expression operand = call.astOperand(); |
| if (operand != null) { |
| ResolvedNode resolved = mContext.resolve(operand); |
| //noinspection SuspiciousMethodCalls |
| if (resolved != null && mVariables.contains(resolved)) { |
| return true; |
| } else if (resolved instanceof ResolvedMethod |
| && operand instanceof MethodInvocation |
| && isCommittedInChainedCalls(mContext,(MethodInvocation) operand)) { |
| // Check that the target of the committed chains is the |
| // right variable! |
| while (operand instanceof MethodInvocation) { |
| operand = ((MethodInvocation)operand).astOperand(); |
| } |
| if (operand instanceof VariableReference) { |
| resolved = mContext.resolve(operand); |
| //noinspection SuspiciousMethodCalls |
| if (resolved != null && mVariables.contains(resolved)) { |
| return true; |
| } |
| } |
| } |
| } |
| } else if (isShowFragmentMethodCall(mContext, call)) { |
| StrictListAccessor<Expression, MethodInvocation> arguments = |
| call.astArguments(); |
| if (arguments.size() == 2) { |
| Expression first = arguments.first(); |
| ResolvedNode resolved = mContext.resolve(first); |
| //noinspection SuspiciousMethodCalls |
| if (resolved != null && mVariables.contains(resolved)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| }; |
| |
| method.accept(commitVisitor); |
| if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) { |
| return true; |
| } |
| } |
| |
| String message = "This transaction should be completed with a `commit()` call"; |
| context.report(COMMIT_FRAGMENT, node, context.getLocation(node.astName()), |
| message); |
| } |
| return false; |
| } |
| |
| private static boolean isCommittedInChainedCalls(@NonNull JavaContext context, |
| @NonNull MethodInvocation 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(); |
| Node parent = node.getParent(); |
| while (parent instanceof MethodInvocation) { |
| MethodInvocation methodInvocation = (MethodInvocation) parent; |
| if (isTransactionCommitMethodCall(context, methodInvocation) |
| || isShowFragmentMethodCall(context, methodInvocation)) { |
| return true; |
| } |
| |
| parent = parent.getParent(); |
| } |
| |
| return false; |
| } |
| |
| private static boolean isTransactionCommitMethodCall(@NonNull JavaContext context, |
| @NonNull MethodInvocation call) { |
| |
| String methodName = call.astName().astValue(); |
| return (COMMIT.equals(methodName) || COMMIT_ALLOWING_LOSS.equals(methodName)) && |
| isMethodOnFragmentClass(context, call, |
| FRAGMENT_TRANSACTION_CLS, |
| FRAGMENT_TRANSACTION_V4_CLS); |
| } |
| |
| private static boolean isShowFragmentMethodCall(@NonNull JavaContext context, |
| @NonNull MethodInvocation call) { |
| String methodName = call.astName().astValue(); |
| return SHOW.equals(methodName) |
| && isMethodOnFragmentClass(context, call, |
| DIALOG_FRAGMENT, DIALOG_V4_FRAGMENT); |
| } |
| |
| private static boolean isMethodOnFragmentClass( |
| @NonNull JavaContext context, |
| @NonNull MethodInvocation call, |
| @NonNull String fragmentClass, |
| @NonNull String v4FragmentClass) { |
| ResolvedNode resolved = context.resolve(call); |
| if (resolved instanceof ResolvedMethod) { |
| ResolvedClass containingClass = ((ResolvedMethod) resolved).getContainingClass(); |
| return containingClass.isSubclassOf(fragmentClass, false) || |
| containingClass.isSubclassOf(v4FragmentClass, false); |
| } |
| |
| return false; |
| } |
| |
| @Nullable |
| public static ResolvedVariable getVariable(@NonNull JavaContext context, |
| @NonNull Node expression) { |
| Node parent = expression.getParent(); |
| if (parent instanceof BinaryExpression) { |
| BinaryExpression binaryExpression = (BinaryExpression) parent; |
| if (binaryExpression.astOperator() == BinaryOperator.ASSIGN) { |
| Expression lhs = binaryExpression.astLeft(); |
| ResolvedNode resolved = context.resolve(lhs); |
| if (resolved instanceof ResolvedVariable) { |
| return (ResolvedVariable) resolved; |
| } |
| } |
| } else if (parent instanceof VariableDefinitionEntry) { |
| ResolvedNode resolved = context.resolve(parent); |
| if (resolved instanceof ResolvedVariable) { |
| return (ResolvedVariable) resolved; |
| } |
| } |
| |
| return null; |
| } |
| |
| private static boolean isBeginTransaction(@NonNull JavaContext context, |
| @NonNull MethodInvocation node) { |
| String methodName = node.astName().astValue(); |
| assert methodName.equals(BEGIN_TRANSACTION) : methodName; |
| if (BEGIN_TRANSACTION.equals(methodName)) { |
| ResolvedNode resolved = context.resolve(node); |
| if (resolved instanceof ResolvedMethod) { |
| ResolvedMethod method = (ResolvedMethod) resolved; |
| ResolvedClass containingClass = method.getContainingClass(); |
| if (containingClass.isSubclassOf(FRAGMENT_MANAGER_CLS, false) |
| || containingClass.isSubclassOf(FRAGMENT_MANAGER_V4_CLS, |
| false)) { |
| 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 ForwardingAstVisitor { |
| protected final JavaContext mContext; |
| protected final List<ResolvedVariable> mVariables; |
| private boolean mContainsCleanup; |
| private boolean mEscapes; |
| |
| public FinishVisitor(JavaContext context, @NonNull ResolvedVariable variable) { |
| mContext = context; |
| mVariables = Lists.newArrayList(variable); |
| } |
| |
| public boolean isCleanedUp() { |
| return mContainsCleanup; |
| } |
| |
| public boolean variableEscapes() { |
| return mEscapes; |
| } |
| |
| @Override |
| public boolean visitNode(Node node) { |
| return mContainsCleanup || super.visitNode(node); |
| } |
| |
| protected abstract boolean isCleanupCall(@NonNull MethodInvocation call); |
| |
| @Override |
| public boolean visitMethodInvocation(MethodInvocation call) { |
| if (mContainsCleanup) { |
| return true; |
| } |
| |
| super.visitMethodInvocation(call); |
| |
| // Look for escapes |
| if (!mEscapes) { |
| for (Expression expression : call.astArguments()) { |
| if (expression instanceof VariableReference) { |
| ResolvedNode resolved = mContext.resolve(expression); |
| //noinspection SuspiciousMethodCalls |
| if (resolved != null && mVariables.contains(resolved)) { |
| 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.astName().astValue())) { |
| ResolvedNode r = mContext.resolve(call); |
| if (r instanceof ResolvedMethod) { |
| ResolvedMethod method = (ResolvedMethod) r; |
| ResolvedClass cls = method.getContainingClass(); |
| if (cls.matches(MOTION_EVENT_CLS)) { |
| mEscapes = false; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if (isCleanupCall(call)) { |
| mContainsCleanup = true; |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) { |
| Expression initializer = node.astInitializer(); |
| if (initializer instanceof VariableReference) { |
| ResolvedNode resolved = mContext.resolve(initializer); |
| //noinspection SuspiciousMethodCalls |
| if (resolved != null && mVariables.contains(resolved)) { |
| ResolvedNode resolvedVariable = mContext.resolve(node); |
| if (resolvedVariable instanceof ResolvedVariable) { |
| ResolvedVariable variable = (ResolvedVariable) resolvedVariable; |
| mVariables.add(variable); |
| } else if (resolvedVariable instanceof ResolvedField) { |
| mEscapes = true; |
| } |
| } |
| } |
| return super.visitVariableDefinitionEntry(node); |
| } |
| |
| @Override |
| public boolean visitBinaryExpression(BinaryExpression node) { |
| if (node.astOperator() == BinaryOperator.ASSIGN) { |
| Expression rhs = node.astRight(); |
| if (rhs instanceof VariableReference) { |
| ResolvedNode resolved = mContext.resolve(rhs); |
| //noinspection SuspiciousMethodCalls |
| if (resolved != null && mVariables.contains(resolved)) { |
| ResolvedNode resolvedLhs = mContext.resolve(node.astLeft()); |
| if (resolvedLhs instanceof ResolvedVariable) { |
| ResolvedVariable variable = (ResolvedVariable) resolvedLhs; |
| mVariables.add(variable); |
| } else if (resolvedLhs instanceof ResolvedField) { |
| mEscapes = true; |
| } |
| } |
| } |
| } |
| return super.visitBinaryExpression(node); |
| } |
| |
| @Override |
| public boolean visitReturn(Return node) { |
| Expression value = node.astValue(); |
| if (value instanceof VariableReference) { |
| ResolvedNode resolved = mContext.resolve(value); |
| //noinspection SuspiciousMethodCalls |
| if (resolved != null && mVariables.contains(resolved)) { |
| mEscapes = true; |
| } |
| } |
| |
| return super.visitReturn(node); |
| } |
| } |
| } |