/* * deadmethods - A unused methods detector * Copyright 2011-2017 MeBigFatGuy.com * Copyright 2011-2017 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.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; 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 { private static final String DEFAULT_REFLECTIVE_ANNOTATION_PATH = "/com/mebigfatguy/deadmethods/defaultReflectiveAnnotations.properties"; Path path; Path auxPath; Set<IgnoredPackage> ignoredPackages = new HashSet(); Set<IgnoredClass> ignoredClasses = new HashSet(); Set<IgnoredMethod> ignoredMethods = new HashSet(); Set<ReflectiveAnnotation> reflectiveAnnotations = new HashSet(); public void addConfiguredClasspath(final Path classpath) { path = classpath; } public void addConfiguredAuxClasspath(final Path auxClassPath) { auxPath = auxClassPath; } public IgnoredPackage createIgnoredPackage() { IgnoredPackage ip = new IgnoredPackage(); ignoredPackages.add(ip); return ip; } public IgnoredClass createIgnoredClass() { IgnoredClass ic = new IgnoredClass(); ignoredClasses.add(ic); return ic; } public IgnoredMethod createIgnoredMethod() { IgnoredMethod im = new IgnoredMethod(); ignoredMethods.add(im); return im; } 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()); } loadDefaultReflectiveAnnotations(); TaskFactory.setTask(this); ClassRepository repo = new ClassRepository(path, auxPath); Set<String> allMethods = new TreeSet(); try { classloop: for (String className : repo) { if (!className.startsWith("[")) { ClassInfo classInfo = repo.getClassInfo(className); String packageName = classInfo.getPackageName(); for (IgnoredPackage ip : ignoredPackages) { Matcher m = ip.getPattern().matcher(packageName); if (m.matches()) { continue classloop; } } String clsName = classInfo.getClassName(); for (IgnoredClass ic : ignoredClasses) { Matcher m = ic.getPattern().matcher(clsName); if (m.matches()) { continue classloop; } } Set<MethodInfo> methods = classInfo.getMethodInfo(); add: for (MethodInfo methodInfo : methods) { for (IgnoredMethod im : ignoredMethods) { Matcher m = im.getPattern().matcher(methodInfo.getMethodName()); if (m.matches()) { continue add; } } 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); removeAnonymousInnerImplementationMethods(repo, allMethods); removeSyntheticMethods(repo, allMethods); removeStandardEnumMethods(repo, allMethods); removeSpecialSerializableMethods(repo, allMethods); removeAnnotations(repo, allMethods); removeSpringMethods(repo, allMethods); removeSPIClasses(repo, allMethods); removeWebMethods(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 loadDefaultReflectiveAnnotations() { BufferedInputStream bis = new BufferedInputStream(FindDeadMethods.class.getResourceAsStream(DEFAULT_REFLECTIVE_ANNOTATION_PATH)); try { Properties p = new Properties(); p.load(bis); for (Object k : p.keySet()) { ReflectiveAnnotation ra = new ReflectiveAnnotation(); ra.setName(k.toString().trim()); reflectiveAnnotations.add(ra); } } catch (IOException e) { // just go on assuming no annotations } finally { Closer.close(bis); } } 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 static 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 static 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(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 static 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()) { if (classInfo.hasAnnotations()) { for (ReflectiveAnnotation ra : reflectiveAnnotations) { if (classInfo.hasAnnotation(ra.toString())) { for (MethodInfo methodInfo : classInfo.getMethodInfo()) { if ((methodInfo.getMethodAccess() & Opcodes.ACC_PUBLIC) != 0) { methods.remove(classInfo.getClassName() + ":" + methodInfo); } } break; } } } for (MethodInfo methodInfo : classInfo.getMethodInfo()) { if (methodInfo.hasAnnotations()) { for (ReflectiveAnnotation ra : reflectiveAnnotations) { if (methodInfo.hasAnnotation(ra.toString())) { methods.remove(classInfo.getClassName() + ":" + methodInfo); break; } } } } } } 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 removeAnonymousInnerImplementationMethods(ClassRepository repo, Set<String> methods) throws IOException { for (ClassInfo classInfo : repo.getAllClassInfos()) { if (classInfo.isAnonymous()) { for (MethodInfo methodInfo : classInfo.getMethodInfo()) { clearDerivedMethods(methods, classInfo, methodInfo.toString()); } } } } private static 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 static 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 static 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) { try { removeSpringMethodsFromXML(repo, methods); removeSpringMethodsFromAnnotations(repo, methods); } catch (Exception e) { throw new BuildException("Failed removing spring methods", e); } } private void removeSpringMethodsFromXML(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() + ".xml"; BufferedInputStream bis = null; try { bis = new BufferedInputStream(repo.getStream(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 removeSpringMethodsFromAnnotations(ClassRepository repo, Set<String> methods) { for (ClassInfo classInfo : repo.getAllClassInfos()) { for (MethodInfo methodInfo : classInfo.getMethodInfo()) { if ("<init>".equals(methodInfo.getMethodName()) && methodInfo.hasAnnotation("org.springframework.beans.factory.annotation.Autowired")) { methods.remove(classInfo.getClassName() + ":" + methodInfo); } } } } private static void removeSPIClasses(ClassRepository repo, Set<String> methods) throws IOException { Iterator<String> spiIterator = repo.serviceIterator(); while (spiIterator.hasNext()) { String fileName = spiIterator.next(); BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(repo.getStream(fileName), "UTF-8")); String clsName = br.readLine(); if (clsName != null) { clsName = clsName.replaceAll("\\.", "/"); for (MethodInfo m : repo.getMethodInfo(clsName)) { if ((m.getMethodAccess() & Opcodes.ACC_PUBLIC) != 0) { String methodInfo = clsName.replaceAll("\\.", "/") + ":" + m.getMethodName() + m.getMethodSignature(); methods.remove(methodInfo); } } } } catch (UnsupportedEncodingException e) { } finally { Closer.close(br); } } } private void removeWebMethods(ClassRepository repo, Set<String> methods) throws IOException { ClassInfo info = repo.getClassInfo("javax/servlet/http/HttpServlet"); for (MethodInfo methodInfo : info.getMethodInfo()) { clearDerivedMethods(methods, info, methodInfo.toString()); } } 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); } } /** for testing only */ public static void main(String[] args) { if (args.length < 1) { throw new IllegalArgumentException("args (" + Arrays.toString(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"); IgnoredPackage ip = fdm.createIgnoredPackage(); ip.setPattern("test\\.ignored"); fdm.execute(); } }