All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.checks.CleanupDetector Maven / Gradle / Ivy

There is a newer version: 25.3.0
Show newest version
/*
 * 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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
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 org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.Interpreter;
import org.objectweb.asm.tree.analysis.Value;

import java.util.Arrays;
import java.util.List;

/**
 * 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 ClassScanner {

    private static final Implementation IMPLEMENTATION = new Implementation(
            CleanupDetector.class,
            Scope.CLASS_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 OBTAIN = "obtain";                                    //$NON-NLS-1$
    private static final String SHOW = "show";                                        //$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$

    // Target owners
    private static final String VELOCITY_TRACKER_CLS = "android/view/VelocityTracker";//$NON-NLS-1$
    private static final String TYPED_ARRAY_CLS = "android/content/res/TypedArray";   //$NON-NLS-1$
    private static final String CONTEXT_CLS = "android/content/Context";              //$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 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$
    private static final String DIALOG_FRAGMENT_SHOW_DESC =
            "(Landroid/app/FragmentTransaction;Ljava/lang/String;)I";                 //$NON-NLS-1$
    private static final String DIALOG_FRAGMENT_SHOW_V4_DESC =
            "(Landroid/support/v4/app/FragmentTransaction;Ljava/lang/String;)I";      //$NON-NLS-1$

    // Target description signatures
    private static final String TYPED_ARRAY_SIG = "Landroid/content/res/TypedArray;"; //$NON-NLS-1$

    private boolean mObtainsTypedArray;
    private boolean mRecyclesTypedArray;
    private boolean mObtainsTracker;
    private boolean mRecyclesTracker;
    private boolean mObtainsMotionEvent;
    private boolean mRecyclesMotionEvent;
    private boolean mObtainsParcel;
    private boolean mRecyclesParcel;
    private boolean mObtainsTransaction;
    private boolean mCommitsTransaction;

    /** Constructs a new {@link CleanupDetector} */
    public CleanupDetector() {
    }

    @Override
    public void afterCheckProject(@NonNull Context context) {
        int phase = context.getDriver().getPhase();
        if (phase == 1) {
            if (mObtainsTypedArray && !mRecyclesTypedArray
                    || mObtainsTracker && !mRecyclesTracker
                    || mObtainsParcel && !mRecyclesParcel
                    || mObtainsMotionEvent && !mRecyclesMotionEvent
                    || mObtainsTransaction && !mCommitsTransaction) {
                context.getDriver().requestRepeat(this, Scope.CLASS_FILE_SCOPE);
            }
        }
    }

    // ---- Implements ClassScanner ----

    @Override
    @Nullable
    public List getApplicableCallNames() {
        return Arrays.asList(
                RECYCLE,
                OBTAIN_STYLED_ATTRIBUTES,
                OBTAIN,
                OBTAIN_ATTRIBUTES,
                OBTAIN_TYPED_ARRAY,
                OBTAIN_NO_HISTORY,
                BEGIN_TRANSACTION,
                COMMIT,
                COMMIT_ALLOWING_LOSS,
                SHOW
        );
    }

    @Override
    public void checkCall(
            @NonNull ClassContext context,
            @NonNull ClassNode classNode,
            @NonNull MethodNode method,
            @NonNull MethodInsnNode call) {
        String name = call.name;
        String owner = call.owner;
        String desc = call.desc;
        int phase = context.getDriver().getPhase();
        if (SHOW.equals(name)) {
            if (desc.equals(DIALOG_FRAGMENT_SHOW_DESC)
                    || desc.equals(DIALOG_FRAGMENT_SHOW_V4_DESC)) {
                mCommitsTransaction = true;
            }
        } else if (RECYCLE.equals(name) && desc.equals("()V")) { //$NON-NLS-1$
            if (owner.equals(TYPED_ARRAY_CLS)) {
                mRecyclesTypedArray = true;
            } else if (owner.equals(VELOCITY_TRACKER_CLS)) {
                mRecyclesTracker = true;
            } else if (owner.equals(MOTION_EVENT_CLS)) {
                mRecyclesMotionEvent = true;
            } else if (owner.equals(PARCEL_CLS)) {
                mRecyclesParcel = true;
            }
        } else if ((COMMIT.equals(name) || COMMIT_ALLOWING_LOSS.equals(name))
                && desc.equals("()I")) { //$NON-NLS-1$
            if (owner.equals(FRAGMENT_TRANSACTION_CLS)
                    || owner.equals(FRAGMENT_TRANSACTION_V4_CLS)) {
                mCommitsTransaction = true;
            }
        } else if (owner.equals(MOTION_EVENT_CLS)) {
            if (OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) {
                mObtainsMotionEvent = true;
                if (phase == 2 && !mRecyclesMotionEvent) {
                    context.report(RECYCLE_RESOURCE, method, call, context.getLocation(call),
                            getErrorMessage(MOTION_EVENT_CLS));
                } else if (phase == 1
                        && checkMethodFlow(context, classNode, method, call, MOTION_EVENT_CLS)) {
                    // Already reported error above; don't do global check
                    mRecyclesMotionEvent = true;
                }
            }
        } else if (OBTAIN.equals(name)) {
            if (owner.equals(VELOCITY_TRACKER_CLS)) {
                mObtainsTracker = true;
                if (phase == 2 && !mRecyclesTracker) {
                    context.report(RECYCLE_RESOURCE, method, call, context.getLocation(call),
                            getErrorMessage(VELOCITY_TRACKER_CLS));
                }
            } else if (owner.equals(PARCEL_CLS)) {
                mObtainsParcel = true;
                if (phase == 2 && !mRecyclesParcel) {
                    context.report(RECYCLE_RESOURCE, method, call, context.getLocation(call),
                            getErrorMessage(PARCEL_CLS));
                } else if (phase == 1
                        && checkMethodFlow(context, classNode, method, call, PARCEL_CLS)) {
                    // Already reported error above; don't do global check
                    mRecyclesParcel = true;
                }
            }
        } else if (OBTAIN_STYLED_ATTRIBUTES.equals(name)
                || OBTAIN_ATTRIBUTES.equals(name)
                || OBTAIN_TYPED_ARRAY.equals(name)) {
            if ((owner.equals(CONTEXT_CLS) || owner.equals(RESOURCES_CLS))
                    && desc.endsWith(TYPED_ARRAY_SIG)) {
                mObtainsTypedArray = true;
                if (phase == 2 && !mRecyclesTypedArray) {
                    context.report(RECYCLE_RESOURCE, method, call, context.getLocation(call),
                            getErrorMessage(TYPED_ARRAY_CLS));
                } else if (phase == 1
                        && checkMethodFlow(context, classNode, method, call, TYPED_ARRAY_CLS)) {
                    // Already reported error above; don't do global check
                    mRecyclesTypedArray = true;
                }
            }
        } else if (BEGIN_TRANSACTION.equals(name)
                && (owner.equals(FRAGMENT_MANAGER_CLS) || owner.equals(FRAGMENT_MANAGER_V4_CLS))) {
            mObtainsTransaction = true;
            if (phase == 2 && !mCommitsTransaction) {
                context.report(COMMIT_FRAGMENT, method, call, context.getLocation(call),
                    getErrorMessage(owner.equals(FRAGMENT_MANAGER_CLS)
                            ? FRAGMENT_TRANSACTION_CLS : FRAGMENT_TRANSACTION_V4_CLS));
            } else if (phase == 1
                    && checkMethodFlow(context, classNode, method, call,
                    owner.equals(FRAGMENT_MANAGER_CLS)
                            ? FRAGMENT_TRANSACTION_CLS : FRAGMENT_TRANSACTION_V4_CLS)) {
                // Already reported error above; don't do global check
                mCommitsTransaction = true;
            }
        }
    }

    /** Computes an error message for a missing recycle of the given type */
    private static String getErrorMessage(String owner) {
        if (FRAGMENT_TRANSACTION_CLS.equals(owner) || FRAGMENT_TRANSACTION_V4_CLS.equals(owner)) {
            return "This transaction should be completed with a `commit()` call";
        }
        String className = owner.substring(owner.lastIndexOf('/') + 1);
        return String.format("This `%1$s` should be recycled after use with `#recycle()`",
                className);
    }

    /**
     * Ensures that the given allocate call in the given method has a
     * corresponding recycle method, also within the same method, OR, the
     * allocated resource flows out of the method (either as a return value, or
     * into a field, or into some other method (with some known exceptions; e.g.
     * passing a MotionEvent into another MotionEvent's constructor is fine)
     * 

* Returns true if an error was found */ private static boolean checkMethodFlow(ClassContext context, ClassNode classNode, MethodNode method, MethodInsnNode call, String recycleOwner) { CleanupTracker interpreter = new CleanupTracker(context, method, call, recycleOwner); ResourceAnalyzer analyzer = new ResourceAnalyzer(interpreter); interpreter.setAnalyzer(analyzer); try { analyzer.analyze(classNode.name, method); if (!interpreter.isCleanedUp() && !interpreter.isEscaped()) { Location location = context.getLocation(call); String message = getErrorMessage(recycleOwner); Issue issue = call.owner.equals(FRAGMENT_MANAGER_CLS) ? COMMIT_FRAGMENT : RECYCLE_RESOURCE; context.report(issue, method, call, location, message); return true; } } catch (AnalyzerException e) { context.log(e, null); } return false; } @VisibleForTesting static boolean hasReturnType(String owner, String desc) { int descLen = desc.length(); int ownerLen = owner.length(); if (descLen < ownerLen + 3) { return false; } if (desc.charAt(descLen - 1) != ';') { return false; } int typeBegin = descLen - 2 - ownerLen; if (desc.charAt(typeBegin - 1) != ')' || desc.charAt(typeBegin) != 'L') { return false; } return desc.regionMatches(typeBegin + 1, owner, 0, ownerLen); } /** * ASM interpreter which tracks the instances of the allocated resource, and * checks whether it is eventually passed to a {@code recycle()} call. If the * value flows out of the method (to a field, or a method call), it will * also consider the resource recycled. */ private static class CleanupTracker extends Interpreter { // Only identity matters, not value private static final Value INSTANCE = BasicValue.INT_VALUE; private static final Value RECYCLED = BasicValue.FLOAT_VALUE; private static final Value UNKNOWN = BasicValue.UNINITIALIZED_VALUE; private final ClassContext mContext; private final MethodNode mMethod; private final MethodInsnNode mObtainNode; private boolean mIsCleanedUp; private boolean mEscapes; private final String mRecycleOwner; private ResourceAnalyzer mAnalyzer; public CleanupTracker( @NonNull ClassContext context, @NonNull MethodNode method, @NonNull MethodInsnNode obtainNode, @NonNull String recycleOwner) { super(Opcodes.ASM4); mContext = context; mMethod = method; mObtainNode = obtainNode; mRecycleOwner = recycleOwner; } /** * Sets the analyzer associated with the interpreter, such that it can * get access to the execution frames */ void setAnalyzer(ResourceAnalyzer analyzer) { mAnalyzer = analyzer; } /** * Returns whether a recycle call was found for the given method * * @return true if the resource was recycled */ public boolean isCleanedUp() { return mIsCleanedUp; } /** * Returns whether the target resource escapes from the method, for * example as a return value, or a field assignment, or getting passed * to another method * * @return true if the resource escapes */ public boolean isEscaped() { return mEscapes; } @Override public Value newOperation(AbstractInsnNode node) throws AnalyzerException { return UNKNOWN; } @Override public Value newValue(final Type type) { if (type != null && type.getSort() == Type.VOID) { return null; } else { return UNKNOWN; } } @Override public Value copyOperation(AbstractInsnNode node, Value value) throws AnalyzerException { return value; } @Override public Value binaryOperation(AbstractInsnNode node, Value value1, Value value2) throws AnalyzerException { if (node.getOpcode() == Opcodes.PUTFIELD) { if (value2 == INSTANCE) { mEscapes = true; } } return merge(value1, value2); } @Override public Value naryOperation(AbstractInsnNode node, List values) throws AnalyzerException { if (node == mObtainNode) { return INSTANCE; } MethodInsnNode call = null; if (node.getType() == AbstractInsnNode.METHOD_INSN) { call = (MethodInsnNode) node; if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) { if (call.name.equals(RECYCLE) && call.owner.equals(mRecycleOwner)) { if (values != null && values.size() == 1 && values.get(0) == INSTANCE) { mIsCleanedUp = true; Frame frame = mAnalyzer.getCurrentFrame(); if (frame != null) { int localSize = frame.getLocals(); for (int i = 0; i < localSize; i++) { Value local = frame.getLocal(i); if (local == INSTANCE) { frame.setLocal(i, RECYCLED); } } int stackSize = frame.getStackSize(); if (stackSize == 1 && frame.getStack(0) == INSTANCE) { frame.pop(); frame.push(RECYCLED); } } return RECYCLED; } } else if ((call.name.equals(COMMIT) || call.name.equals(COMMIT_ALLOWING_LOSS)) && call.owner.equals(mRecycleOwner)) { if (values != null && values.size() == 1 && values.get(0) == INSTANCE) { mIsCleanedUp = true; return INSTANCE; } } else if (call.name.equals(SHOW) && ( call.desc.equals(DIALOG_FRAGMENT_SHOW_DESC) || call.desc.equals(DIALOG_FRAGMENT_SHOW_V4_DESC))) { if (values != null && values.size() == 3 && values.get(1) == INSTANCE) { mIsCleanedUp = true; return INSTANCE; } } else if (call.owner.equals(mRecycleOwner) && hasReturnType(mRecycleOwner, call.desc)) { // Called method which returns self. This helps handle cases where you call // createTransaction().method1().method2().method3().commit() -- if // method1, 2 and 3 all return "this" then the commit call is really // called on the createTransaction instance return INSTANCE; } } } if (values != null && values.size() >= 1) { // Skip the first element: method calls *on* the TypedArray are okay int start = node.getOpcode() == Opcodes.INVOKESTATIC ? 0 : 1; for (int i = 0, n = values.size(); i < n; i++) { Object v = values.get(i); if (v == INSTANCE && i >= start) { // Known special cases if (node.getOpcode() == Opcodes.INVOKESTATIC) { assert call != null; if (call.name.equals(OBTAIN) && call.owner.equals(MOTION_EVENT_CLS)) { return UNKNOWN; } } // Passing the instance to another method: could leak // the instance out of this method (for example calling // a method which recycles it on our behalf, or store it // in some holder which will recycle it later). In this // case, just assume that things are okay. mEscapes = true; } else if (v == RECYCLED && call != null) { Location location = mContext.getLocation(call); String message = String.format("This `%1$s` has already been recycled", mRecycleOwner.substring(mRecycleOwner.lastIndexOf('/') + 1)); mContext.report(RECYCLE_RESOURCE, mMethod, call, location, message); } } } return UNKNOWN; } @Override public Value unaryOperation(AbstractInsnNode node, Value value) throws AnalyzerException { return value; } @Override public Value ternaryOperation(AbstractInsnNode node, Value value1, Value value2, Value value3) throws AnalyzerException { if (value1 == RECYCLED || value2 == RECYCLED || value3 == RECYCLED) { return RECYCLED; } else if (value1 == INSTANCE || value2 == INSTANCE || value3 == INSTANCE) { return INSTANCE; } return UNKNOWN; } @Override public void returnOperation(AbstractInsnNode node, Value value1, Value value2) throws AnalyzerException { if (value1 == INSTANCE || value2 == INSTANCE) { mEscapes = true; } } @Override public Value merge(Value value1, Value value2) { if (value1 == RECYCLED || value2 == RECYCLED) { return RECYCLED; } else if (value1 == INSTANCE || value2 == INSTANCE) { return INSTANCE; } return UNKNOWN; } } private static class ResourceAnalyzer extends Analyzer { private Frame mCurrent; private Frame mFrame1; private Frame mFrame2; public ResourceAnalyzer(Interpreter interpreter) { super(interpreter); } Frame getCurrentFrame() { return mCurrent; } @Override protected void init(String owner, MethodNode m) throws AnalyzerException { mCurrent = mFrame2; super.init(owner, m); } @Override protected Frame newFrame(int nLocals, int nStack) { // Stash the two most recent frame allocations. When init is called the second // most recently seen frame is the current frame used during execution, which // is where we need to replace INSTANCE with RECYCLED when the void // recycle method is called. Frame newFrame = super.newFrame(nLocals, nStack); mFrame2 = mFrame1; mFrame1 = newFrame; return newFrame; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy