/* * FindBugs - Find Bugs in Java programs * Copyright (C) 2005, University of Maryland * * 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 edu.umd.cs.findbugs.model; import java.io.IOException; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.apache.bcel.Repository; import org.apache.bcel.classfile.Code; import org.apache.bcel.classfile.Field; import org.apache.bcel.classfile.FieldOrMethod; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; import edu.umd.cs.findbugs.ba.Hierarchy; import edu.umd.cs.findbugs.ba.JavaClassAndMethod; import edu.umd.cs.findbugs.ba.SignatureParser; import edu.umd.cs.findbugs.xml.XMLAttributeList; import edu.umd.cs.findbugs.xml.XMLOutput; import edu.umd.cs.findbugs.xml.XMLWriteable; /** * Features of a class which may be used to identify it if it is renamed * or modified. * * @author David Hovemeyer */ public class ClassFeatureSet implements XMLWriteable { public static final String CLASS_NAME_KEY = "Class:"; public static final String METHOD_NAME_KEY = "Method:"; public static final String CODE_LENGTH_KEY = "CodeLength:"; public static final String FIELD_NAME_KEY = "Field:"; private String className; private boolean isInterface; private Set<String> featureSet; /** * Constructor. * Creates an empty feature set. */ public ClassFeatureSet() { this.featureSet = new HashSet<String>(); } /** * Minimum code length required to add a CodeLength feature. */ public static final int MIN_CODE_LENGTH = 10; /** * Initialize from given JavaClass. * * @param javaClass the JavaClass * @return this object */ public ClassFeatureSet initialize(JavaClass javaClass) { this.className = javaClass.getClassName(); this.isInterface = javaClass.isInterface(); addFeature(CLASS_NAME_KEY + transformClassName(javaClass.getClassName())); for (Method method : javaClass.getMethods()) { if (!isSynthetic(method)) { String transformedMethodSignature = transformMethodSignature(method.getSignature()); if (method.isStatic() || !overridesSuperclassMethod(javaClass, method)) { addFeature(METHOD_NAME_KEY + method.getName() + ":" + transformedMethodSignature); } Code code = method.getCode(); if (code != null && code.getCode() != null && code.getCode().length >= MIN_CODE_LENGTH) { addFeature(CODE_LENGTH_KEY + method.getName() + ":" + transformedMethodSignature + ":" + code.getCode().length); } } } for (Field field : javaClass.getFields()) { if (!isSynthetic(field)) { addFeature( FIELD_NAME_KEY + field.getName() + ":" + transformSignature(field.getSignature())); } } return this; } /** * Determine if given method overrides a superclass or superinterface method. * * @param javaClass class defining the method * @param method the method * @return true if the method overrides a superclass/superinterface method, false if not * @throws ClassNotFoundException */ private boolean overridesSuperclassMethod(JavaClass javaClass, Method method) { if (method.isStatic()) return false; try { JavaClass[] superclassList = javaClass.getSuperClasses(); if (superclassList != null) { JavaClassAndMethod match = Hierarchy.findMethod(superclassList, method.getName(), method.getSignature(), Hierarchy.INSTANCE_METHOD); if (match != null) return true; } JavaClass[] interfaceList = javaClass.getAllInterfaces(); if (interfaceList != null) { JavaClassAndMethod match = Hierarchy.findMethod(interfaceList, method.getName(), method.getSignature(), Hierarchy.INSTANCE_METHOD); if (match != null) return true; } return false; } catch (ClassNotFoundException e) { return true; } } /** * Figure out if a class member (field or method) is synthetic. * * @param member a field or method * @return true if the member is synthetic */ private boolean isSynthetic(FieldOrMethod member) { if (member.isSynthetic()) // this never works, but worth a try return true; String name = member.getName(); if (name.startsWith("class$")) return true; if (name.startsWith("access$")) return true; return false; } /** * @return Returns the className. */ public String getClassName() { return className; } /** * @param className The className to set. */ public void setClassName(String className) { this.className = className; } /** * @return Returns the isInterface. */ public boolean isInterface() { return isInterface; } /** * @param isInterface The isInterface to set. */ public void setInterface(boolean isInterface) { this.isInterface = isInterface; } public int getNumFeatures() { return featureSet.size(); } public void addFeature(String feature) { featureSet.add(feature); } public Iterator<String> featureIterator() { return featureSet.iterator(); } public boolean hasFeature(String feature) { return featureSet.contains(feature); } /** * Transform a class name by stripping its package name. * * @param className a class name * @return the transformed class name */ public static String transformClassName(String className) { int lastDot = className.lastIndexOf('.'); if (lastDot >= 0) { String pkg = className.substring(0, lastDot); if (!isUnlikelyToBeRenamed(pkg)) { className = className.substring(lastDot + 1); } } return className; } /** * Return true if classes in the given package is unlikely to be renamed: * e.g., because they are part of a public API. * * @param pkg the package name * @return true if classes in the package is unlikely to be renamed */ public static boolean isUnlikelyToBeRenamed(String pkg) { return pkg.startsWith("java."); } /** * Transform a method signature to allow it to be compared even if * any of its parameter types are moved to another package. * * @param signature a method signature * @return the transformed signature */ public static String transformMethodSignature(String signature) { StringBuilder buf = new StringBuilder(); buf.append('('); SignatureParser parser = new SignatureParser(signature); for (Iterator<String> i = parser.parameterSignatureIterator(); i.hasNext(); ) { String param = i.next(); param = transformSignature(param); buf.append(param); } buf.append(')'); return buf.toString(); } /** * Transform a field or method parameter signature to allow it to be * compared even if it is moved to another package. * * @param signature the signature * @return the transformed signature */ public static String transformSignature(String signature) { StringBuilder buf = new StringBuilder(); int lastBracket = signature.lastIndexOf('['); if (lastBracket > 0) { buf.append(signature.substring(0, lastBracket+1)); signature = signature.substring(lastBracket+1); } if (signature.startsWith("L")) { signature = signature.substring(1, signature.length() - 1).replace('/', '.'); signature = transformClassName(signature); signature = "L" + signature.replace('.', '/') + ";"; } buf.append(signature); return buf.toString(); } /** * Minimum number of features which must be present in order * to declare two classes similar. */ public static final int MIN_FEATURES = 5; /** * Minimum similarity required to declare two classes similar. */ public static final double MIN_MATCH = 0.60; /** * Similarity of classes which don't have enough features to match * exactly, but whose class names match exactly. */ public static final double EXACT_CLASS_NAME_MATCH = MIN_MATCH + 0.1; public static double similarity(ClassFeatureSet a, ClassFeatureSet b) { // Some features must match exactly if (a.isInterface() != b.isInterface()) return 0.0; if (a.getNumFeatures() < MIN_FEATURES || b.getNumFeatures() < MIN_FEATURES) return a.getClassName().equals(b.getClassName()) ? EXACT_CLASS_NAME_MATCH : 0.0; int numMatch = 0; int max = Math.max(a.getNumFeatures(), b.getNumFeatures()); for (Iterator<String> i = a.featureIterator(); i.hasNext();) { String feature = i.next(); if (b.hasFeature(feature)) { ++numMatch; } } return ((double) numMatch / (double) max); } public boolean similarTo(ClassFeatureSet other) { return similarity(this, other) >= MIN_MATCH; } public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("Usage: " + ClassFeatureSet.class.getName() + " <class 1> <class 2>"); System.exit(1); } JavaClass a = Repository.lookupClass(args[0]); JavaClass b = Repository.lookupClass(args[1]); ClassFeatureSet aFeatures = new ClassFeatureSet().initialize(a); ClassFeatureSet bFeatures = new ClassFeatureSet().initialize(b); System.out.println("Similarity is " + similarity(aFeatures, bFeatures)); System.out.println("Classes are" + (aFeatures.similarTo(bFeatures) ? "" : " not") + " similar"); } public static final String ELEMENT_NAME = "ClassFeatureSet"; public static final String FEATURE_ELEMENT_NAME = "Feature"; /* (non-Javadoc) * @see edu.umd.cs.findbugs.xml.XMLWriteable#writeXML(edu.umd.cs.findbugs.xml.XMLOutput) */ public void writeXML(XMLOutput xmlOutput) throws IOException { xmlOutput.openTag(ELEMENT_NAME, new XMLAttributeList().addAttribute("class", className)); for (Iterator<String> i = featureIterator(); i.hasNext(); ) { String feature = i.next(); xmlOutput.openCloseTag( FEATURE_ELEMENT_NAME, new XMLAttributeList().addAttribute("value", feature)); } xmlOutput.closeTag(ELEMENT_NAME); } }