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

src.main.java.com.mebigfatguy.fbcontrib.detect.DeletingWhileIterating Maven / Gradle / Ivy

Go to download

An auxiliary findbugs.sourceforge.net plugin for java bug detectors that fall outside the narrow scope of detectors to be packaged with the product itself.

There is a newer version: 7.6.8
Show newest version
/*
 * fb-contrib - Auxiliary detectors for Java programs
 * Copyright (C) 2005-2018 Dave Brosius
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.mebigfatguy.fbcontrib.detect;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.bcel.Constants;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LocalVariable;
import org.apache.bcel.classfile.LocalVariableTable;

import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.CodeByteUtils;
import com.mebigfatguy.fbcontrib.utils.OpcodeUtils;
import com.mebigfatguy.fbcontrib.utils.QMethod;
import com.mebigfatguy.fbcontrib.utils.RegisterUtils;
import com.mebigfatguy.fbcontrib.utils.SignatureBuilder;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.TernaryPatcher;
import com.mebigfatguy.fbcontrib.utils.ToString;
import com.mebigfatguy.fbcontrib.utils.UnmodifiableSet;
import com.mebigfatguy.fbcontrib.utils.Values;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.FieldAnnotation;
import edu.umd.cs.findbugs.OpcodeStack;
import edu.umd.cs.findbugs.OpcodeStack.CustomUserValue;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;
import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName;

/**
 * looks for deletion of items from a collection using the remove method of the collection at the same time that the collection is being iterated on. If this
 * occurs the iterator will become invalid and throw a ConcurrentModificationException. Instead, the remove should be called on the iterator itself.
 */
@CustomUserValue
public class DeletingWhileIterating extends AbstractCollectionScanningDetector {
    private static JavaClass iteratorClass;
    private static Set exceptionClasses;

    static {
        try {
            iteratorClass = Repository.lookupClass("java/util/Iterator");
        } catch (ClassNotFoundException cnfe) {
            iteratorClass = null;
        }

        try {
            exceptionClasses = new HashSet<>(2);
            exceptionClasses.add(Repository.lookupClass("java/util/concurrent/CopyOnWriteArrayList"));
            exceptionClasses.add(Repository.lookupClass("java/util/concurrent/CopyOnWriteArraySet"));
        } catch (ClassNotFoundException cnfe) {
            // don't have a bugReporter yet, so do nothing
        }
    }

    private static final Set collectionMethods = UnmodifiableSet.create(
            new QMethod("entrySet", new SignatureBuilder().withReturnType(Values.SLASHED_JAVA_UTIL_SET).toString()),
            new QMethod("keySet", new SignatureBuilder().withReturnType(Values.SLASHED_JAVA_UTIL_SET).toString()),
            new QMethod("values", new SignatureBuilder().withReturnType(Values.SLASHED_JAVA_UTIL_COLLECTION).toString()));

    private static final Map modifyingMethods;

    static {
        Map mm = new HashMap<>();
        mm.put(new QMethod("add", SignatureBuilder.SIG_OBJECT_TO_BOOLEAN), Values.ONE);
        mm.put(new QMethod("addAll", SignatureBuilder.SIG_COLLECTION_TO_PRIMITIVE_BOOLEAN), Values.ONE);
        mm.put(new QMethod("addAll", new SignatureBuilder().withParamTypes(Values.SIG_PRIMITIVE_INT, Values.SLASHED_JAVA_UTIL_COLLECTION)
                .withReturnType(Values.SIG_PRIMITIVE_BOOLEAN).toString()), Values.TWO);
        mm.put(new QMethod("clear", SignatureBuilder.SIG_VOID_TO_VOID), Values.ZERO);
        mm.put(new QMethod("remove", SignatureBuilder.SIG_INT_TO_OBJECT), Values.ONE);
        mm.put(new QMethod("removeAll", SignatureBuilder.SIG_COLLECTION_TO_PRIMITIVE_BOOLEAN), Values.ONE);
        mm.put(new QMethod("retainAll", SignatureBuilder.SIG_COLLECTION_TO_PRIMITIVE_BOOLEAN), Values.ONE);
        modifyingMethods = Collections. unmodifiableMap(mm);
    }

    private static final QMethod ITERATOR = new QMethod("iterator", new SignatureBuilder().withReturnType("java/util/Iterator").toString());
    private static final QMethod REMOVE = new QMethod("remove", SignatureBuilder.SIG_OBJECT_TO_BOOLEAN);
    private static final QMethod HASNEXT = new QMethod("hasNext", SignatureBuilder.SIG_VOID_TO_BOOLEAN);

    private List collectionGroups;
    private Map groupToIterator;
    private Map loops;
    private Map endOfScopes;

    /**
     * constructs a DWI detector given the reporter to report bugs on
     *
     * @param bugReporter
     *            the sync of bug reports
     */
    public DeletingWhileIterating(BugReporter bugReporter) {
        super(bugReporter, Values.SLASHED_JAVA_UTIL_COLLECTION);
    }

    /**
     * implements the visitor to setup the opcode stack, collectionGroups, groupToIterator and loops
     *
     * @param classContext
     *            the context object of the currently parsed class
     */
    @Override
    public void visitClassContext(ClassContext classContext) {
        if ((collectionClass == null) || (iteratorClass == null)) {
            return;
        }

        try {
            collectionGroups = new ArrayList<>();
            groupToIterator = new HashMap<>();
            loops = new HashMap<>(10);
            super.visitClassContext(classContext);
        } finally {
            collectionGroups = null;
            groupToIterator = null;
            loops = null;
            endOfScopes = null;
        }
    }

    /**
     * implements the visitor to reset the stack, collectionGroups, groupToIterator and loops
     *
     * @param obj
     *            the context object of the currently parsed code block
     */
    @Override
    public void visitCode(Code obj) {
        collectionGroups.clear();
        groupToIterator.clear();
        loops.clear();
        buildVariableEndScopeMap();

        super.visitCode(obj);
    }

    /**
     * implements the visitor to look for deletes on collections that are being iterated
     *
     * @param seen
     *            the opcode of the currently parsed instruction
     */
    @Override
    public void sawOpcode(int seen) {
        int groupId = -1;

        try {
            stack.precomputation(this);

            if (seen == INVOKEINTERFACE) {
                String className = getClassConstantOperand();
                String methodName = getNameConstantOperand();
                String signature = getSigConstantOperand();
                QMethod methodInfo = new QMethod(methodName, signature);

                if (isCollection(className)) {
                    if (collectionMethods.contains(methodInfo) || ITERATOR.equals(methodInfo)) {
                        if (stack.getStackDepth() > 0) {
                            OpcodeStack.Item itm = stack.getStackItem(0);
                            groupId = findCollectionGroup(itm, true);
                        }
                    } else if (REMOVE.equals(methodInfo)) {
                        if (stack.getStackDepth() > 1) {
                            OpcodeStack.Item itm = stack.getStackItem(1);
                            int id = findCollectionGroup(itm, true);
                            if ((id >= 0) && collectionGroups.get(id).isStandardCollection()) {
                                Integer it = groupToIterator.get(Integer.valueOf(id));
                                Loop loop = loops.get(it);
                                if (loop != null) {
                                    int pc = getPC();
                                    if (loop.hasPC(pc)) {
                                        boolean needPop = !Values.SIG_VOID.equals(SignatureUtils.getReturnSignature(signature));

                                        if (!breakFollows(loop, needPop) && !returnFollows(needPop)) {
                                            bugReporter.reportBug(new BugInstance(this, BugType.DWI_DELETING_WHILE_ITERATING.name(), NORMAL_PRIORITY)
                                                    .addClass(this).addMethod(this).addSourceLine(this));
                                        }
                                    }
                                }
                            }
                        }
                    } else {
                        Integer numArgs = modifyingMethods.get(methodInfo);
                        if ((numArgs != null) && (stack.getStackDepth() > numArgs.intValue())) {
                            OpcodeStack.Item itm = stack.getStackItem(numArgs.intValue());
                            int id = findCollectionGroup(itm, true);
                            if (id >= 0) {
                                Integer it = groupToIterator.get(Integer.valueOf(id));
                                if (it != null) {
                                    Loop loop = loops.get(it);
                                    if (loop != null) {
                                        int pc = getPC();
                                        if (loop.hasPC(pc)) {
                                            boolean needPop = !Values.SIG_VOID.equals(SignatureUtils.getReturnSignature(signature));
                                            boolean breakFollows = breakFollows(loop, needPop);
                                            boolean returnFollows = !breakFollows && returnFollows(needPop);

                                            if (!breakFollows && !returnFollows) {
                                                bugReporter.reportBug(new BugInstance(this, BugType.DWI_MODIFYING_WHILE_ITERATING.name(), NORMAL_PRIORITY)
                                                        .addClass(this).addMethod(this).addSourceLine(this));
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                } else if ("java/util/Iterator".equals(className) && HASNEXT.equals(methodInfo) && (stack.getStackDepth() > 0)) {
                    OpcodeStack.Item itm = stack.getStackItem(0);
                    Integer id = (Integer) itm.getUserValue();
                    if (id != null) {
                        groupId = id.intValue();
                    }
                }
            } else if ((seen == PUTFIELD) || (seen == PUTSTATIC)) {
                if (stack.getStackDepth() > 1) {
                    OpcodeStack.Item itm = stack.getStackItem(0);

                    Integer id = (Integer) itm.getUserValue();
                    if (id == null) {
                        FieldAnnotation fa = FieldAnnotation
                                .fromFieldDescriptor(new FieldDescriptor(getClassConstantOperand(), getNameConstantOperand(), getSigConstantOperand(), false));
                        itm = new OpcodeStack.Item(itm.getSignature(), fa, stack.getStackItem(1).getRegisterNumber());
                        removeFromCollectionGroup(itm);
                        groupId = findCollectionGroup(itm, true);
                    }
                }
            } else if (OpcodeUtils.isAStore(seen)) {
                if (stack.getStackDepth() > 0) {
                    OpcodeStack.Item itm = stack.getStackItem(0);
                    Integer id = (Integer) itm.getUserValue();
                    if (id != null) {
                        int reg = RegisterUtils.getAStoreReg(this, seen);

                        try {
                            JavaClass cls = itm.getJavaClass();
                            if ((cls != null) && cls.implementationOf(iteratorClass)) {
                                Integer regIt = Integer.valueOf(reg);
                                Iterator curIt = groupToIterator.values().iterator();
                                while (curIt.hasNext()) {
                                    if (curIt.next().equals(regIt)) {
                                        curIt.remove();
                                    }
                                }
                                groupToIterator.put(id, regIt);
                            }

                            GroupPair pair = collectionGroups.get(id.intValue());
                            if (pair != null) {
                                pair.addMember(Integer.valueOf(reg));
                            }
                        } catch (ClassNotFoundException cnfe) {
                            bugReporter.reportMissingClass(cnfe);
                        }
                    } else {
                        String cls = itm.getSignature();
                        if ((cls != null) && cls.startsWith(Values.SIG_QUALIFIED_CLASS_PREFIX)) {
                            cls = SignatureUtils.trimSignature(cls);
                            if (isCollection(cls) || "java/util/Iterator".equals(cls)) {
                                int reg = RegisterUtils.getAStoreReg(this, seen);
                                removeFromCollectionGroup(new OpcodeStack.Item(itm, reg));
                                Iterator it = groupToIterator.values().iterator();
                                while (it.hasNext()) {
                                    if (it.next().intValue() == reg) {
                                        it.remove();
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            } else if (OpcodeUtils.isALoad(seen)) {
                int reg = RegisterUtils.getALoadReg(this, seen);
                OpcodeStack.Item itm = new OpcodeStack.Item(new OpcodeStack.Item(), reg);
                groupId = findCollectionGroup(itm, false);
            } else if ((seen == IFEQ) && (stack.getStackDepth() > 0)) {
                OpcodeStack.Item itm = stack.getStackItem(0);
                Integer id = (Integer) itm.getUserValue();
                if (id != null) {
                    int target = getBranchTarget();
                    int gotoAddr = target - 3;
                    int ins = getCode().getCode()[gotoAddr];
                    if (ins < 0) {
                        ins = 256 + ins;
                    }
                    if ((ins == GOTO) || (ins == GOTO_W)) {
                        Integer reg = groupToIterator.get(id);
                        if (reg != null) {
                            loops.put(reg, new Loop(getPC(), gotoAddr));
                        }
                    }
                }
            }
        } finally {
            TernaryPatcher.pre(stack, seen);
            stack.sawOpcode(this, seen);
            TernaryPatcher.post(stack, seen);
            if ((groupId >= 0) && (stack.getStackDepth() > 0)) {
                OpcodeStack.Item itm = stack.getStackItem(0);
                itm.setUserValue(Integer.valueOf(groupId));
            }

            processEndOfScopes(Integer.valueOf(getPC()));
        }
    }

    /**
     * looks to see if the following instruction is a GOTO, preceded by potentially a pop
     *
     * @param loop
     *            the loop structure we are checking
     * @param needsPop
     *            whether we expect to see a pop next
     *
     * @return whether a GOTO is found
     */
    private boolean breakFollows(Loop loop, boolean needsPop) {

        byte[] code = getCode().getCode();
        int nextPC = getNextPC();

        if (needsPop) {
            int popOp = CodeByteUtils.getbyte(code, nextPC++);
            if (popOp != Constants.POP) {
                return false;
            }
        }

        int gotoOp = CodeByteUtils.getbyte(code, nextPC);
        if ((gotoOp == Constants.GOTO) || (gotoOp == Constants.GOTO_W)) {
            int target = nextPC + CodeByteUtils.getshort(code, nextPC + 1);
            if (target > loop.getLoopFinish()) {
                return true;
            }
        }

        return false;
    }

    /**
     * This attempts to see if there is some form of a return statement following the collection modifying statement in the loop. It is a bad cheat, because, we
     * may allow a POP, or an ALOAD/ILOAD etc before the return. this is sloppy tho as it might be a multibyte instruction. It also might be a complex piece of
     * code to load the return, or the method may not allow returns. But hopefully it's better than it was.
     *
     * @param couldSeePop
     *            if the preceding instruction returns a value, and thus might need to be popped
     *
     * @return when a following instruction issues some sort of return
     */
    private boolean returnFollows(boolean couldSeePop) {

        byte[] code = getCode().getCode();
        int nextPC = getNextPC();

        int nextOp = CodeByteUtils.getbyte(code, nextPC++);

        if ((nextOp >= Constants.IRETURN) && (nextOp <= Constants.RETURN)) {
            return true;
        } else if ((couldSeePop) && (nextOp == Constants.POP)) {
            nextOp = CodeByteUtils.getbyte(code, nextPC++);
            if ((nextOp >= Constants.IRETURN) && (nextOp <= Constants.RETURN)) {
                return true;
            }
        }

        nextOp = CodeByteUtils.getbyte(code, nextPC++);
        return (nextOp >= Constants.IRETURN) && (nextOp <= Constants.RETURN);
    }

    /**
     * returns whether the class name is derived from java.util.Collection
     *
     * @param className
     *            the class to check
     * @return whether the class is a collection
     */
    private boolean isCollection(@SlashedClassName String className) {
        try {
            JavaClass cls = Repository.lookupClass(className);
            return cls.implementationOf(collectionClass) && !exceptionClasses.contains(cls);
        } catch (ClassNotFoundException cnfe) {
            bugReporter.reportMissingClass(cnfe);
            return false;
        }
    }

    /**
     * given an register or field, look to see if this thing is associated with an already discovered loop
     *
     * @param itm
     *            the item containing the register or field
     * @return the group element
     */
    private static Comparable getGroupElement(OpcodeStack.Item itm) {
        Comparable groupElement = null;

        int reg = itm.getRegisterNumber();
        if (reg >= 0) {
            groupElement = Integer.valueOf(reg);
        } else {
            XField field = itm.getXField();
            if (field != null) {
                int regLoad = itm.getFieldLoadedFromRegister();
                if (regLoad >= 0) {
                    groupElement = field.getName() + ":{" + regLoad + '}';
                }
            }
        }

        return groupElement;
    }

    private int findCollectionGroup(OpcodeStack.Item itm, boolean addIfNotFound) {

        Integer id = (Integer) itm.getUserValue();
        if (id != null) {
            return id.intValue();
        }

        Comparable groupElement = getGroupElement(itm);
        if (groupElement == null) {
            return -1;
        }
        int numGroups = collectionGroups.size();
        for (int i = 0; i < numGroups; i++) {
            GroupPair groupPair = collectionGroups.get(i);
            if (groupPair.containsMember(groupElement)) {
                return i;
            }
        }

        if (!addIfNotFound) {
            return -1;
        }

        GroupPair groupPair = new GroupPair(groupElement, itm.getSignature());
        collectionGroups.add(groupPair);
        return collectionGroups.size() - 1;
    }

    private void removeFromCollectionGroup(OpcodeStack.Item itm) {
        Comparable groupElement = getGroupElement(itm);
        if (groupElement != null) {
            for (GroupPair groupPair : collectionGroups) {
                if (groupPair.containsMember(groupElement)) {
                    groupPair.removeMember(groupElement);
                    break;
                }
            }
        }
    }

    private void buildVariableEndScopeMap() {
        endOfScopes = new HashMap<>();

        LocalVariableTable lvt = getMethod().getLocalVariableTable();
        if (lvt != null) {
            int len = lvt.getLength();
            for (int i = 0; i < len; i++) {
                @SuppressWarnings("deprecation")
                LocalVariable lv = lvt.getLocalVariable(i);
                if (lv != null) {
                    Integer endPC = Integer.valueOf(lv.getStartPC() + lv.getLength());
                    BitSet vars = endOfScopes.get(endPC);
                    if (vars == null) {
                        vars = new BitSet();
                        endOfScopes.put(endPC, vars);
                    }
                    vars.set(lv.getIndex());
                }
            }
        }
    }

    private void processEndOfScopes(Integer pc) {
        BitSet endVars = endOfScopes.get(pc);
        if (endVars != null) {
            for (int i = endVars.nextSetBit(0); i >= 0; i = endVars.nextSetBit(i + 1)) {
                Integer v = Integer.valueOf(i);
                {
                    Iterator it = collectionGroups.iterator();
                    while (it.hasNext()) {
                        GroupPair groupPair = it.next();
                        if (groupPair.containsMember(v)) {
                            groupPair.removeMember(v);
                        }
                    }
                }
                {
                    Iterator it = groupToIterator.values().iterator();
                    while (it.hasNext()) {
                        if (v.equals(it.next())) {
                            it.remove();
                        }
                    }
                }
            }
        }
    }

    /**
     * represents a simple loop
     */
    static class Loop {
        public int loopStart;
        public int loopFinish;

        public Loop(int start, int finish) {
            loopStart = start;
            loopFinish = finish;
        }

        int getLoopFinish() {
            return loopFinish;
        }

        int getLoopStart() {
            return loopStart;
        }

        boolean hasPC(int pc) {
            return (loopStart <= pc) && (pc <= loopFinish);
        }

        @Override
        public String toString() {
            return ToString.build(this);
        }
    }

    /**
     * represents aliases of some kind to some sort of a collection, or a related object like a keySet, or an iterator
     */
    static class GroupPair {
        private final Set> groupMembers;
        private final String colClass;

        public GroupPair(Comparable member, String cls) {
            groupMembers = new HashSet<>();
            groupMembers.add(member);
            colClass = cls;
        }

        void addMember(Comparable member) {
            groupMembers.add(member);
        }

        void removeMember(Comparable member) {
            groupMembers.remove(member);
        }

        boolean containsMember(Comparable member) {
            return groupMembers.contains(member);
        }

        boolean isStandardCollection() {
            return (colClass == null) || !colClass.contains("/concurrent/");
        }

        @Override
        public String toString() {
            return ToString.build(this);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy