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

src.main.java.com.mebigfatguy.fbcontrib.detect.OverlyPermissiveMethod 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.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

import org.apache.bcel.Constants;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantCP;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantInvokeDynamic;
import org.apache.bcel.classfile.ConstantMethodHandle;
import org.apache.bcel.classfile.ConstantNameAndType;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Unknown;

import com.mebigfatguy.fbcontrib.collect.MethodInfo;
import com.mebigfatguy.fbcontrib.collect.Statistics;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.FQMethod;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.ToString;
import com.mebigfatguy.fbcontrib.utils.Values;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.OpcodeStack;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;

/**
 * looks for methods that are declared more permissively than the code is using. For instance, declaring a method public, when it could just be declared
 * private.
 */
public class OverlyPermissiveMethod extends BytecodeScanningDetector {

    private static Map DECLARED_ACCESS = new HashMap<>();

    static {
        DECLARED_ACCESS.put(Integer.valueOf(Constants.ACC_PRIVATE), "private");
        DECLARED_ACCESS.put(Integer.valueOf(Constants.ACC_PROTECTED), "protected");
        DECLARED_ACCESS.put(Integer.valueOf(Constants.ACC_PUBLIC), "public");
        DECLARED_ACCESS.put(Integer.valueOf(0), "package private");
    }

    private BugReporter bugReporter;
    private OpcodeStack stack;
    private JavaClass cls;
    private String callingPackage;
    private String callingClass;

    /**
     * constructs a OPM detector given the reporter to report bugs on
     *
     * @param bugReporter
     *            the sync of bug reports
     */
    public OverlyPermissiveMethod(BugReporter bugReporter) {
        this.bugReporter = bugReporter;
    }

    @Override
    public void visitClassContext(ClassContext classContext) {
        try {
            cls = classContext.getJavaClass();
            ClassDescriptor cd = classContext.getClassDescriptor();
            callingClass = cd.getClassName();
            callingPackage = cd.getPackageName();
            stack = new OpcodeStack();
            super.visitClassContext(classContext);
        } finally {
            callingPackage = null;
            stack = null;
        }
    }

    @Override
    public void visitCode(Code obj) {
        Method m = getMethod();
        String methodName = m.getName();
        String sig = m.getSignature();

        if (isAssumedPublic(methodName)) {
            MethodInfo mi = Statistics.getStatistics().getMethodStatistics(cls.getClassName(), methodName, sig);
            mi.addCallingAccess(Constants.ACC_PUBLIC);
        } else {
            if (!hasRuntimeAnnotations(m) && !isGetterSetter(methodName, sig)) {
                MethodInfo mi = Statistics.getStatistics().getMethodStatistics(cls.getClassName(), methodName, sig);
                mi.addCallingAccess(Constants.ACC_PUBLIC);
            }
            stack.resetForMethodEntry(this);
            super.visitCode(obj);
        }
    }

    @Override
    public void sawOpcode(int seen) {
        try {
            stack.precomputation(this);

            switch (seen) {
                case INVOKEVIRTUAL:
                case INVOKEINTERFACE:
                case INVOKESTATIC:
                case INVOKESPECIAL: {
                    String calledClass = getClassConstantOperand();
                    String sig = getSigConstantOperand();
                    MethodInfo mi = Statistics.getStatistics().getMethodStatistics(calledClass, getNameConstantOperand(), sig);
                    if (mi != null) {
                        if (seen == INVOKEINTERFACE) {
                            mi.addCallingAccess(Constants.ACC_PUBLIC);
                        } else {
                            String calledPackage;
                            int slashPos = calledClass.lastIndexOf('/');
                            if (slashPos >= 0) {
                                calledPackage = calledClass.substring(0, slashPos);
                            } else {
                                calledPackage = "";
                            }
                            boolean sameClass = calledClass.equals(callingClass);
                            boolean samePackage = calledPackage.equals(callingPackage);

                            if (sameClass) {
                                mi.addCallingAccess(Constants.ACC_PRIVATE);
                            } else if (samePackage) {
                                mi.addCallingAccess(0);
                            } else {
                                if (seen == INVOKESTATIC) {
                                    mi.addCallingAccess(Constants.ACC_PUBLIC);
                                } else if (isCallingOnThis(sig)) {
                                    mi.addCallingAccess(Constants.ACC_PROTECTED);
                                } else {
                                    mi.addCallingAccess(Constants.ACC_PUBLIC);
                                }
                            }
                        }
                    }
                }
                break;

                case INVOKEDYNAMIC:
                    // smells like a hack. Not sure how to do this better as bcel and findbugs are older than dirt
                    ConstantInvokeDynamic id = (ConstantInvokeDynamic) getConstantRefOperand();

                    BootstrapMethod bm = getBootstrapMethod(id.getBootstrapMethodAttrIndex());
                    if (bm != null) {
                        ConstantPool pool = getConstantPool();
                        ConstantMethodHandle mh = bm.getFirstMethodHandle(pool);
                        if (mh != null) {
                            ConstantCP ref = (ConstantCP) pool.getConstant(mh.getReferenceIndex());
                            ConstantClass cc = (ConstantClass) pool.getConstant(ref.getClassIndex());
                            String clz = ((ConstantUtf8) pool.getConstant(cc.getNameIndex())).getBytes();
                            ConstantNameAndType nameAndType = (ConstantNameAndType) pool.getConstant(ref.getNameAndTypeIndex());
                            String sig = ((ConstantUtf8) pool.getConstant(nameAndType.getSignatureIndex())).getBytes();
                            String name = ((ConstantUtf8) pool.getConstant(nameAndType.getNameIndex())).getBytes();
                            MethodInfo mi = Statistics.getStatistics().getMethodStatistics(clz, name, sig);
                            mi.addCallingAccess(Constants.ACC_PUBLIC);
                        }
                    }

                break;

                default:
                break;
            }
        } finally {
            stack.sawOpcode(this, seen);
        }
    }

    private boolean hasRuntimeAnnotations(Method obj) {
        AnnotationEntry[] annotations = obj.getAnnotationEntries();
        if (annotations != null) {
            for (AnnotationEntry entry : annotations) {
                if (entry.isRuntimeVisible()) {
                    return true;
                }
            }
        }

        return false;
    }

    private boolean isAssumedPublic(String methodName) {
        return (cls.isEnum() && "valueOf".equals(methodName));
    }

    private boolean isGetterSetter(String methodName, String methodSignature) {
        if (methodName.startsWith("get") || methodName.startsWith("set")) {
            int numParameters = SignatureUtils.getNumParameters(methodSignature);
            boolean voidReturn = Values.SIG_VOID.equals(SignatureUtils.getReturnSignature(methodSignature));

            if ((numParameters == 0) && !voidReturn && (methodName.charAt(0) == 'g')) {
                return true;
            }

            if ((numParameters == 1) && voidReturn && (methodName.charAt(0) == 's')) {
                return true;
            }
        }

        return false;
    }

    /**
     * checks to see if an instance method is called on the 'this' object
     *
     * @param sig
     *            the signature of the method called to find the called-on object
     * @return when it is called on this or not
     */
    private boolean isCallingOnThis(String sig) {
        if (getMethod().isStatic()) {
            return false;
        }

        int numParameters = SignatureUtils.getNumParameters(sig);
        if (stack.getStackDepth() <= numParameters) {
            return false;
        }

        OpcodeStack.Item item = stack.getStackItem(numParameters);
        return item.getRegisterNumber() == 0;
    }

    /**
     * after collecting all method calls, build a report of all methods that have been called, but in a way that is less permissive then is defined.
     */
    @Override
    public void report() {
        for (Map.Entry entry : Statistics.getStatistics()) {
            MethodInfo mi = entry.getValue();

            int declaredAccess = mi.getDeclaredAccess();
            if ((declaredAccess & Constants.ACC_PRIVATE) != 0) {
                continue;
            }

            if (mi.wasCalledPublicly() || !mi.wasCalled()) {
                continue;
            }

            FQMethod key = entry.getKey();

            String methodName = key.getMethodName();
            if (isGetterSetter(methodName, key.getSignature())) {
                continue;
            }

            if (isOverlyPermissive(declaredAccess) && !isConstrainedByInterface(key)) {
                try {
                    String clsName = key.getClassName();
                    if (!isDerived(Repository.lookupClass(clsName), key)) {

                        BugInstance bi = new BugInstance(this, BugType.OPM_OVERLY_PERMISSIVE_METHOD.name(), LOW_PRIORITY).addClass(clsName).addMethod(clsName,
                                key.getMethodName(), key.getSignature(), (declaredAccess & Constants.ACC_STATIC) != 0);

                        String descr = String.format("- Method declared %s but could be declared %s", getDeclaredAccessValue(declaredAccess),
                                getRequiredAccessValue(mi));
                        bi.addString(descr);

                        bugReporter.reportBug(bi);
                    }
                } catch (ClassNotFoundException cnfe) {
                    bugReporter.reportMissingClass(cnfe);
                }
            }
        }
    }

    private static boolean isOverlyPermissive(int declaredAccess) {
        return (declaredAccess & Constants.ACC_PUBLIC) != 0;
    }

    /**
     * looks to see if this method is an implementation of a method in an interface, including generic specified interface methods.
     *
     * @param fqMethod
     *            the method to check
     * @return if this method is constrained by an interface method
     */
    private boolean isConstrainedByInterface(FQMethod fqMethod) {

        try {
            JavaClass fqCls = Repository.lookupClass(fqMethod.getClassName());
            if (fqCls.isInterface()) {
                return true;
            }

            for (JavaClass inf : fqCls.getAllInterfaces()) {
                for (Method infMethod : inf.getMethods()) {
                    if (infMethod.getName().equals(fqMethod.getMethodName())) {
                        String infMethodSig = infMethod.getSignature();
                        String fqMethodSig = fqMethod.getSignature();
                        if (infMethodSig.equals(fqMethodSig)) {
                            return true;
                        }

                        List infTypes = SignatureUtils.getParameterSignatures(infMethodSig);
                        List fqTypes = SignatureUtils.getParameterSignatures(fqMethodSig);

                        if (infTypes.size() == fqTypes.size()) {
                            boolean matches = true;
                            for (int i = 0; i < infTypes.size(); i++) {
                                String infParmType = infTypes.get(i);
                                String fqParmType = fqTypes.get(i);
                                if (infParmType.equals(fqParmType)) {
                                    if ((infParmType.charAt(0) != 'L') || (fqParmType.charAt(0) != 'L')) {
                                        matches = false;
                                        break;
                                    }

                                    JavaClass infParmClass = Repository.lookupClass(SignatureUtils.stripSignature(infParmType));
                                    JavaClass fqParmClass = Repository.lookupClass(SignatureUtils.stripSignature(fqParmType));
                                    if (!fqParmClass.instanceOf(infParmClass)) {
                                        matches = false;
                                        break;
                                    }
                                }
                            }

                            if (matches) {
                                return true;
                            }
                        }
                    }
                }
            }

            return false;
        } catch (ClassNotFoundException cnfe) {
            bugReporter.reportMissingClass(cnfe);
            return true;
        }
    }

    /**
     * looks to see if this method described by key is derived from a superclass or interface
     *
     * @param fqCls
     *            the class that the method is defined in
     * @param key
     *            the information about the method
     * @return whether this method derives from something or not
     */
    private boolean isDerived(JavaClass fqCls, FQMethod key) {
        try {
            for (JavaClass infCls : fqCls.getInterfaces()) {
                for (Method infMethod : infCls.getMethods()) {
                    if (key.getMethodName().equals(infMethod.getName())) {
                        if (infMethod.getGenericSignature() != null) {
                            if (SignatureUtils.compareGenericSignature(infMethod.getGenericSignature(), key.getSignature())) {
                                return true;
                            }
                        } else if (infMethod.getSignature().equals(key.getSignature())) {
                            return true;
                        }
                    }
                }
            }

            JavaClass superClass = fqCls.getSuperClass();
            if ((superClass == null) || Values.DOTTED_JAVA_LANG_OBJECT.equals(superClass.getClassName())) {
                return false;
            }

            for (Method superMethod : superClass.getMethods()) {
                if (key.getMethodName().equals(superMethod.getName())) {
                    if (superMethod.getGenericSignature() != null) {
                        if (SignatureUtils.compareGenericSignature(superMethod.getGenericSignature(), key.getSignature())) {
                            return true;
                        }
                    } else if (superMethod.getSignature().equals(key.getSignature())) {
                        return true;
                    }
                }
            }

            return isDerived(superClass, key);
        } catch (ClassNotFoundException cnfe) {
            bugReporter.reportMissingClass(cnfe);
            return true;
        }
    }

    private static String getDeclaredAccessValue(int declaredAccess) {
        return DECLARED_ACCESS.get(Integer.valueOf(declaredAccess & (Constants.ACC_PRIVATE | Constants.ACC_PROTECTED | Constants.ACC_PUBLIC)));
    }

    private static Object getRequiredAccessValue(MethodInfo mi) {
        if (mi.wasCalledProtectedly()) {
            return "protected";
        }
        if (mi.wasCalledPackagely()) {
            return "package private";
        }
        return "private";
    }

    @Nullable
    private BootstrapMethod getBootstrapMethod(int bootstrapIndex) {
        for (Attribute a : cls.getAttributes()) {
            if ("BootstrapMethods".equals(a.getName())) {
                if (a instanceof Unknown) {
                    Unknown ua = (Unknown) a;
                    byte[] data = ua.getBytes();
                    ByteBuffer buffer = ByteBuffer.wrap(data, 0, data.length);
                    int numMethods = buffer.getShort();
                    if (bootstrapIndex >= numMethods) {
                        return null;
                    }

                    for (int i = 0; i < numMethods; i++) {
                        BootstrapMethod m = new BootstrapMethod(buffer);
                        if (i == bootstrapIndex) {
                            return m;
                        }
                    }
                } else {
                    throw new RuntimeException(
                            "Incompatible bcel version, apparently bcel has been upgraded to not use 'Unknown' for 'BootstrapMethods', but uses: "
                                    + a.getName());
                }
                return null;
            }
        }

        return null;
    }

    /**
     * represents a bootstrap method
     */
    static class BootstrapMethod {
        int bootstrapMethodRef;
        int[] args;

        public BootstrapMethod(ByteBuffer buffer) {
            bootstrapMethodRef = buffer.getShort();
            int numBootstrapArgs = buffer.getShort();
            args = new int[numBootstrapArgs];
            for (int a = 0; a < numBootstrapArgs; a++) {
                args[a] = buffer.getShort();
            }
        }

        public int getBootstrapMethodRef() {
            return bootstrapMethodRef;
        }

        public int[] getArgs() {
            return args;
        }

        @Nullable
        public ConstantMethodHandle getFirstMethodHandle(ConstantPool pool) {
            for (int arg : args) {
                Constant c = pool.getConstant(arg);
                if (c instanceof ConstantMethodHandle) {
                    return ((ConstantMethodHandle) c);
                }
            }

            return null;
        }

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy