com.mebigfatguy.deadmethods.FindDeadMethods.java Source code

Java tutorial

Introduction

Here is the source code for com.mebigfatguy.deadmethods.FindDeadMethods.java

Source

/*
 * deadmethods - A unused methods detector
 * Copyright 2011-2012 MeBigFatGuy.com
 * Copyright 2011-2012 Dave Brosius
 *
 * 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.mebigfatguy.deadmethods;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Path;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class FindDeadMethods extends Task {
    Path path;
    Path auxPath;
    Set<String> ignorePackages;
    Set<ReflectiveAnnotation> reflectiveAnnotations = new HashSet<ReflectiveAnnotation>();

    public void addConfiguredClasspath(final Path classpath) {
        path = classpath;
    }

    public void addConfiguredAuxClasspath(final Path auxClassPath) {
        auxPath = auxClassPath;
    }

    public void setIgnorePackages(String packages) {
        String[] packs = packages.split("\\s*,\\s*");
        ignorePackages = new HashSet<String>(Arrays.asList(packs));
    }

    public ReflectiveAnnotation createReflectiveAnnotation() {
        ReflectiveAnnotation ra = new ReflectiveAnnotation();
        reflectiveAnnotations.add(ra);
        return ra;
    }

    @Override
    public void execute() throws BuildException {
        if (path == null) {
            throw new BuildException("classpath attribute not set");
        }

        if (auxPath == null) {
            auxPath = new Path(getProject());
        }

        if (ignorePackages == null) {
            ignorePackages = new HashSet<String>();
        }

        TaskFactory.setTask(this);

        ClassRepository repo = new ClassRepository(path, auxPath);
        Set<String> allMethods = new TreeSet<String>();
        try {
            for (String className : repo) {
                if (!className.startsWith("[")) {
                    ClassInfo classInfo = repo.getClassInfo(className);
                    String packageName = classInfo.getPackageName();
                    if (!ignorePackages.contains(packageName)) {
                        Set<MethodInfo> methods = classInfo.getMethodInfo();

                        for (MethodInfo methodInfo : methods) {
                            allMethods.add(
                                    className + ":" + methodInfo.getMethodName() + methodInfo.getMethodSignature());
                        }
                    }
                }
            }

            removeObjectMethods(repo, allMethods);
            removeMainMethods(repo, allMethods);
            removeNoArgCtors(repo, allMethods);
            removeJUnitMethods(repo, allMethods);
            removeReflectiveAnnotatedMethods(repo, allMethods);
            removeInterfaceImplementationMethods(repo, allMethods);
            removeSyntheticMethods(repo, allMethods);
            removeStandardEnumMethods(repo, allMethods);
            removeSpecialSerializableMethods(repo, allMethods);
            removeAnnotations(repo, allMethods);
            removeSpringMethods(repo, allMethods);

            for (String className : repo) {
                InputStream is = null;
                try {
                    is = repo.getClassStream(className);

                    ClassReader r = new ClassReader(is);
                    r.accept(new CalledMethodRemovingClassVisitor(repo, allMethods), ClassReader.SKIP_DEBUG);
                } finally {
                    Closer.close(is);
                }
            }

            for (String m : allMethods) {
                System.out.println(m);
            }
        } catch (Exception ioe) {
            throw new BuildException("Failed collecting methods: " + ioe.getMessage(), ioe);
        }
    }

    private void removeObjectMethods(ClassRepository repo, Set<String> methods) throws IOException {
        ClassInfo info = repo.getClassInfo("java/lang/Object");
        for (MethodInfo methodInfo : info.getMethodInfo()) {
            clearDerivedMethods(methods, info, methodInfo.toString());
        }
    }

    private void removeMainMethods(ClassRepository repo, Set<String> methods) {
        MethodInfo mainInfo = new MethodInfo("main", "([Ljava/lang/String;)V", Opcodes.ACC_STATIC);
        for (ClassInfo classInfo : repo.getAllClassInfos()) {
            Set<MethodInfo> methodInfo = classInfo.getMethodInfo();
            if (methodInfo.contains(mainInfo)) {
                methods.remove(classInfo.getClassName() + ":main([Ljava/lang/String;)V");
            }
        }
    }

    private void removeNoArgCtors(ClassRepository repo, Set<String> methods) {
        MethodInfo ctorInfo = new MethodInfo("<init>", "()V", Opcodes.ACC_STATIC);
        for (ClassInfo classInfo : repo.getAllClassInfos()) {
            Set<String> infs = new HashSet<String>(Arrays.asList(classInfo.getInterfaceNames()));
            if (infs.contains("java/lang/Serializable")) {
                Set<MethodInfo> methodInfo = classInfo.getMethodInfo();
                if (methodInfo.contains(ctorInfo)) {
                    methods.remove(classInfo.getClassName() + ":" + methodInfo);
                }
            }
        }
    }

    private void removeJUnitMethods(ClassRepository repo, Set<String> methods) {
        for (ClassInfo classInfo : repo.getAllClassInfos()) {
            for (MethodInfo methodInfo : classInfo.getMethodInfo()) {
                if (methodInfo.isTest()) {
                    methods.remove(classInfo.getClassName() + ":" + methodInfo);
                }
            }
        }
    }

    private void removeReflectiveAnnotatedMethods(ClassRepository repo, Set<String> methods) {
        for (ClassInfo classInfo : repo.getAllClassInfos()) {
            for (MethodInfo methodInfo : classInfo.getMethodInfo()) {
                if (methodInfo.hasAnnotations()) {
                    for (ReflectiveAnnotation ra : reflectiveAnnotations) {
                        if (methodInfo.hasAnnotation(ra.toString())) {
                            methods.remove(classInfo.getClassName() + ":" + methodInfo);
                        }
                    }
                }
            }
        }
    }

    private void removeInterfaceImplementationMethods(ClassRepository repo, Set<String> methods)
            throws IOException {
        for (ClassInfo classInfo : repo.getAllClassInfos()) {
            if (classInfo.isInterface()) {
                for (MethodInfo methodInfo : classInfo.getMethodInfo()) {
                    clearDerivedMethods(methods, classInfo, methodInfo.toString());
                }
            }
        }
    }

    private void removeSyntheticMethods(ClassRepository repo, Set<String> methods) {
        for (ClassInfo classInfo : repo.getAllClassInfos()) {
            for (MethodInfo methodInfo : classInfo.getMethodInfo()) {
                if (methodInfo.isSynthetic()) {
                    methods.remove(classInfo.getClassName() + ":" + methodInfo);
                }
            }
        }
    }

    private void removeStandardEnumMethods(ClassRepository repo, Set<String> methods) throws IOException {
        ClassInfo info = repo.getClassInfo("java/lang/Enum");
        {
            MethodInfo methodInfo = new MethodInfo("valueOf", "(Ljava/lang/String;)?", Opcodes.ACC_PUBLIC);
            clearDerivedMethods(methods, info, methodInfo.toString());
        }
        {
            MethodInfo methodInfo = new MethodInfo("values", "()[?", Opcodes.ACC_PUBLIC);
            clearDerivedMethods(methods, info, methodInfo.toString());
        }
    }

    private void removeSpecialSerializableMethods(ClassRepository repo, Set<String> methods) {
        for (ClassInfo classInfo : repo.getAllClassInfos()) {
            for (MethodInfo methodInfo : classInfo.getMethodInfo()) {
                if ("writeObject".equals(methodInfo.getMethodName())
                        && "(Ljava/io/ObjectOutputStream;)V".equals(methodInfo.getMethodSignature())) {
                    methods.remove(classInfo.getClassName() + ":" + methodInfo);
                } else if ("readObject".equals(methodInfo.getMethodName())
                        && "(Ljava/io/ObjectInputStream;)V".equals(methodInfo.getMethodSignature())) {
                    methods.remove(classInfo.getClassName() + ":" + methodInfo);
                } else if ("writeExternal".equals(methodInfo.getMethodName())
                        && "(Ljava/io/ObjectOutput;)V".equals(methodInfo.getMethodSignature())) {
                    methods.remove(classInfo.getClassName() + ":" + methodInfo);
                } else if ("readExternal".equals(methodInfo.getMethodName())
                        && "(Ljava/io/ObjectInput;)V".equals(methodInfo.getMethodSignature())) {
                    methods.remove(classInfo.getClassName() + ":" + methodInfo);
                }
            }
        }
    }

    private void removeAnnotations(ClassRepository repo, Set<String> methods) {
        for (ClassInfo classInfo : repo.getAllClassInfos()) {
            if (classInfo.isAnnotation()) {
                for (MethodInfo methodInfo : classInfo.getMethodInfo()) {
                    methods.remove(classInfo.getClassName() + ":" + methodInfo);
                }
            }
        }
    }

    private void removeSpringMethods(ClassRepository repo, Set<String> methods)
            throws ParserConfigurationException, XPathExpressionException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        XPathFactory xpf = XPathFactory.newInstance();
        XPath xp = xpf.newXPath();
        XPathExpression beanExpression = xp.compile("/beans/bean");
        XPathExpression beanClassExpression = xp.compile("@class");
        XPathExpression initMethodExpression = xp.compile("@init-method");
        XPathExpression destroyMethodExpression = xp.compile("@destroy-method");
        XPathExpression propertyExpression = xp.compile("property");
        XPathExpression propertyNameExpression = xp.compile("@name");
        XPathExpression propertyRefExpression = xp.compile("@ref");
        XPathExpression refBeanExpression = xp.compile("ref/@bean");

        Iterator<String> xmlIterator = repo.xmlIterator();
        while (xmlIterator.hasNext()) {
            String xmlName = xmlIterator.next();
            BufferedInputStream bis = null;
            try {
                bis = new BufferedInputStream(repo.getXMLStream(xmlName));
                Document doc = db.parse(bis);

                NodeList beans = (NodeList) beanExpression.evaluate(doc, XPathConstants.NODESET);
                for (int i = 0; i < beans.getLength(); i++) {
                    Element bean = (Element) beans.item(i);
                    Attr beanClass = (Attr) beanClassExpression.evaluate(bean, XPathConstants.NODE);
                    if (beanClass != null) {
                        Attr initMethod = (Attr) initMethodExpression.evaluate(bean, XPathConstants.NODE);
                        Attr destroyMethod = (Attr) destroyMethodExpression.evaluate(bean, XPathConstants.NODE);
                        NodeList properties = (NodeList) propertyExpression.evaluate(bean, XPathConstants.NODESET);

                        ClassInfo classInfo = repo.getClassInfo(beanClass.getValue().replaceAll("\\.", "/"));
                        if (classInfo != null) {
                            if (initMethod != null) {
                                String initMethodName = initMethod.getValue();
                                methods.remove(classInfo.getClassName() + ":" + initMethodName + "()V");
                            }
                            if (destroyMethod != null) {
                                String destroyMethodName = destroyMethod.getValue();
                                methods.remove(classInfo.getClassName() + ":" + destroyMethodName + "()V");
                            }
                            for (int j = 0; j < properties.getLength(); j++) {
                                Element property = (Element) properties.item(j);
                                Attr propertyAttr = (Attr) propertyNameExpression.evaluate(property,
                                        XPathConstants.NODE);
                                String propNameValue = propertyAttr.getValue();
                                Attr refAttr = (Attr) propertyRefExpression.evaluate(property, XPathConstants.NODE);
                                if (refAttr == null) {
                                    refAttr = (Attr) refBeanExpression.evaluate(property, XPathConstants.NODE);
                                }

                                //Don't handle sub xml files thru value attributes yet
                                if (refAttr != null) {
                                    XPathExpression refClassExpression = xp
                                            .compile("/beans/bean[@id='" + refAttr.getValue() + "']/@class");
                                    Attr refClassAttr = (Attr) refClassExpression.evaluate(doc,
                                            XPathConstants.NODE);

                                    if (refClassAttr != null) {
                                        String methodName = "set" + Character.toUpperCase(propNameValue.charAt(0))
                                                + propNameValue.substring(1);
                                        String methodSig = "(L" + refClassAttr.getValue().replaceAll("\\.", "/")
                                                + ";)V";
                                        methods.remove(classInfo.getClassName() + ":" + methodName + methodSig);
                                    }
                                }
                            }
                        }
                    }
                }

            } catch (Exception ioe) {
                log("Failed parsing possible spring bean xml file: " + xmlName);
            } finally {
                Closer.close(bis);
            }
        }
    }

    private void clearDerivedMethods(Set<String> methods, ClassInfo info, String methodInfo) throws IOException {
        Set<ClassInfo> derivedInfos = info.getDerivedClasses();

        for (ClassInfo derivedInfo : derivedInfos) {
            //regex chokes because of the $ in output classname, so do it the old way
            int qMarkPos = methodInfo.indexOf('?');
            String appliedMethodInfo;
            if (qMarkPos >= 0) {
                appliedMethodInfo = methodInfo.substring(0, qMarkPos);
                appliedMethodInfo += "L" + derivedInfo.getClassName() + ";";
                appliedMethodInfo += methodInfo.substring(qMarkPos + 1);
            } else {
                appliedMethodInfo = methodInfo;
            }
            methods.remove(derivedInfo.getClassName() + ":" + appliedMethodInfo);
            clearDerivedMethods(methods, derivedInfo, methodInfo);
        }
    }

    public static class ReflectiveAnnotation {
        private String annotationName;

        public void setName(String name) {
            annotationName = name;
        }

        @Override
        public String toString() {
            return annotationName;
        }
    }

    /** for testing only */
    public static void main(String[] args) {

        if (args.length < 1) {
            throw new IllegalArgumentException("args must contain classpath root");
        }

        FindDeadMethods fdm = new FindDeadMethods();
        Project project = new Project();
        fdm.setProject(project);

        Path path = new Path(project);
        path.setLocation(new File(args[0]));
        fdm.addConfiguredClasspath(path);
        ReflectiveAnnotation ra = fdm.createReflectiveAnnotation();
        ra.setName("test.reflective.ReflectiveUse");
        fdm.setIgnorePackages("test.ignored");

        fdm.execute();
    }
}