001: /*
002: * FindBugs - Find Bugs in Java programs
003: * Copyright (C) 2005, University of Maryland
004: *
005: * This library is free software; you can redistribute it and/or
006: * modify it under the terms of the GNU Lesser General Public
007: * License as published by the Free Software Foundation; either
008: * version 2.1 of the License, or (at your option) any later version.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General Public
016: * License along with this library; if not, write to the Free Software
017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: */
019:
020: package edu.umd.cs.findbugs.model;
021:
022: import java.io.IOException;
023: import java.util.HashSet;
024: import java.util.Iterator;
025: import java.util.Set;
026:
027: import org.apache.bcel.Repository;
028: import org.apache.bcel.classfile.Code;
029: import org.apache.bcel.classfile.Field;
030: import org.apache.bcel.classfile.FieldOrMethod;
031: import org.apache.bcel.classfile.JavaClass;
032: import org.apache.bcel.classfile.Method;
033:
034: import edu.umd.cs.findbugs.ba.Hierarchy;
035: import edu.umd.cs.findbugs.ba.JavaClassAndMethod;
036: import edu.umd.cs.findbugs.ba.SignatureParser;
037: import edu.umd.cs.findbugs.xml.XMLAttributeList;
038: import edu.umd.cs.findbugs.xml.XMLOutput;
039: import edu.umd.cs.findbugs.xml.XMLWriteable;
040:
041: /**
042: * Features of a class which may be used to identify it if it is renamed
043: * or modified.
044: *
045: * @author David Hovemeyer
046: */
047: public class ClassFeatureSet implements XMLWriteable {
048: public static final String CLASS_NAME_KEY = "Class:";
049: public static final String METHOD_NAME_KEY = "Method:";
050: public static final String CODE_LENGTH_KEY = "CodeLength:";
051: public static final String FIELD_NAME_KEY = "Field:";
052:
053: private String className;
054: private boolean isInterface;
055: private Set<String> featureSet;
056:
057: /**
058: * Constructor.
059: * Creates an empty feature set.
060: */
061: public ClassFeatureSet() {
062: this .featureSet = new HashSet<String>();
063: }
064:
065: /**
066: * Minimum code length required to add a CodeLength feature.
067: */
068: public static final int MIN_CODE_LENGTH = 10;
069:
070: /**
071: * Initialize from given JavaClass.
072: *
073: * @param javaClass the JavaClass
074: * @return this object
075: */
076: public ClassFeatureSet initialize(JavaClass javaClass) {
077: this .className = javaClass.getClassName();
078: this .isInterface = javaClass.isInterface();
079:
080: addFeature(CLASS_NAME_KEY
081: + transformClassName(javaClass.getClassName()));
082:
083: for (Method method : javaClass.getMethods()) {
084: if (!isSynthetic(method)) {
085: String transformedMethodSignature = transformMethodSignature(method
086: .getSignature());
087:
088: if (method.isStatic()
089: || !overridesSuperclassMethod(javaClass, method)) {
090: addFeature(METHOD_NAME_KEY + method.getName() + ":"
091: + transformedMethodSignature);
092: }
093:
094: Code code = method.getCode();
095: if (code != null && code.getCode() != null
096: && code.getCode().length >= MIN_CODE_LENGTH) {
097: addFeature(CODE_LENGTH_KEY + method.getName() + ":"
098: + transformedMethodSignature + ":"
099: + code.getCode().length);
100: }
101: }
102: }
103:
104: for (Field field : javaClass.getFields()) {
105: if (!isSynthetic(field)) {
106: addFeature(FIELD_NAME_KEY + field.getName() + ":"
107: + transformSignature(field.getSignature()));
108: }
109: }
110:
111: return this ;
112: }
113:
114: /**
115: * Determine if given method overrides a superclass or superinterface method.
116: *
117: * @param javaClass class defining the method
118: * @param method the method
119: * @return true if the method overrides a superclass/superinterface method, false if not
120: * @throws ClassNotFoundException
121: */
122: private boolean overridesSuperclassMethod(JavaClass javaClass,
123: Method method) {
124: if (method.isStatic())
125: return false;
126:
127: try {
128: JavaClass[] super classList = javaClass.getSuperClasses();
129: if (super classList != null) {
130: JavaClassAndMethod match = Hierarchy.findMethod(
131: super classList, method.getName(), method
132: .getSignature(),
133: Hierarchy.INSTANCE_METHOD);
134: if (match != null)
135: return true;
136: }
137:
138: JavaClass[] interfaceList = javaClass.getAllInterfaces();
139: if (interfaceList != null) {
140: JavaClassAndMethod match = Hierarchy.findMethod(
141: interfaceList, method.getName(), method
142: .getSignature(),
143: Hierarchy.INSTANCE_METHOD);
144: if (match != null)
145: return true;
146: }
147:
148: return false;
149: } catch (ClassNotFoundException e) {
150: return true;
151: }
152: }
153:
154: /**
155: * Figure out if a class member (field or method) is synthetic.
156: *
157: * @param member a field or method
158: * @return true if the member is synthetic
159: */
160: private boolean isSynthetic(FieldOrMethod member) {
161: if (member.isSynthetic()) // this never works, but worth a try
162: return true;
163:
164: String name = member.getName();
165:
166: if (name.startsWith("class$"))
167: return true;
168:
169: if (name.startsWith("access$"))
170: return true;
171:
172: return false;
173: }
174:
175: /**
176: * @return Returns the className.
177: */
178: public String getClassName() {
179: return className;
180: }
181:
182: /**
183: * @param className The className to set.
184: */
185: public void setClassName(String className) {
186: this .className = className;
187: }
188:
189: /**
190: * @return Returns the isInterface.
191: */
192: public boolean isInterface() {
193: return isInterface;
194: }
195:
196: /**
197: * @param isInterface The isInterface to set.
198: */
199: public void setInterface(boolean isInterface) {
200: this .isInterface = isInterface;
201: }
202:
203: public int getNumFeatures() {
204: return featureSet.size();
205: }
206:
207: public void addFeature(String feature) {
208: featureSet.add(feature);
209: }
210:
211: public Iterator<String> featureIterator() {
212: return featureSet.iterator();
213: }
214:
215: public boolean hasFeature(String feature) {
216: return featureSet.contains(feature);
217: }
218:
219: /**
220: * Transform a class name by stripping its package name.
221: *
222: * @param className a class name
223: * @return the transformed class name
224: */
225: public static String transformClassName(String className) {
226: int lastDot = className.lastIndexOf('.');
227: if (lastDot >= 0) {
228: String pkg = className.substring(0, lastDot);
229: if (!isUnlikelyToBeRenamed(pkg)) {
230: className = className.substring(lastDot + 1);
231: }
232: }
233: return className;
234: }
235:
236: /**
237: * Return true if classes in the given package is unlikely to be renamed:
238: * e.g., because they are part of a public API.
239: *
240: * @param pkg the package name
241: * @return true if classes in the package is unlikely to be renamed
242: */
243: public static boolean isUnlikelyToBeRenamed(String pkg) {
244: return pkg.startsWith("java.");
245: }
246:
247: /**
248: * Transform a method signature to allow it to be compared even if
249: * any of its parameter types are moved to another package.
250: *
251: * @param signature a method signature
252: * @return the transformed signature
253: */
254: public static String transformMethodSignature(String signature) {
255: StringBuffer buf = new StringBuffer();
256:
257: buf.append('(');
258:
259: SignatureParser parser = new SignatureParser(signature);
260: for (Iterator<String> i = parser.parameterSignatureIterator(); i
261: .hasNext();) {
262: String param = i.next();
263: param = transformSignature(param);
264: buf.append(param);
265: }
266:
267: buf.append(')');
268:
269: return buf.toString();
270: }
271:
272: /**
273: * Transform a field or method parameter signature to allow it to be
274: * compared even if it is moved to another package.
275: *
276: * @param signature the signature
277: * @return the transformed signature
278: */
279: public static String transformSignature(String signature) {
280: StringBuffer buf = new StringBuffer();
281:
282: int lastBracket = signature.lastIndexOf('[');
283: if (lastBracket > 0) {
284: buf.append(signature.substring(0, lastBracket + 1));
285: signature = signature.substring(lastBracket + 1);
286: }
287:
288: if (signature.startsWith("L")) {
289: signature = signature.substring(1, signature.length() - 1)
290: .replace('/', '.');
291: signature = transformClassName(signature);
292: signature = "L" + signature.replace('.', '/') + ";";
293: }
294: buf.append(signature);
295:
296: return buf.toString();
297: }
298:
299: /**
300: * Minimum number of features which must be present in order
301: * to declare two classes similar.
302: */
303: public static final int MIN_FEATURES = 5;
304:
305: /**
306: * Minimum similarity required to declare two classes similar.
307: */
308: public static final double MIN_MATCH = 0.60;
309:
310: /**
311: * Similarity of classes which don't have enough features to match
312: * exactly, but whose class names match exactly.
313: */
314: public static final double EXACT_CLASS_NAME_MATCH = MIN_MATCH + 0.1;
315:
316: public static double similarity(ClassFeatureSet a, ClassFeatureSet b) {
317: // Some features must match exactly
318: if (a.isInterface() != b.isInterface())
319: return 0.0;
320:
321: if (a.getNumFeatures() < MIN_FEATURES
322: || b.getNumFeatures() < MIN_FEATURES)
323: return a.getClassName().equals(b.getClassName()) ? EXACT_CLASS_NAME_MATCH
324: : 0.0;
325:
326: int numMatch = 0;
327: int max = Math.max(a.getNumFeatures(), b.getNumFeatures());
328:
329: for (Iterator<String> i = a.featureIterator(); i.hasNext();) {
330: String feature = i.next();
331: if (b.hasFeature(feature)) {
332: ++numMatch;
333: }
334: }
335:
336: return ((double) numMatch / (double) max);
337:
338: }
339:
340: public boolean similarTo(ClassFeatureSet other) {
341: return similarity(this , other) >= MIN_MATCH;
342: }
343:
344: public static void main(String[] args) throws Exception {
345: if (args.length != 2) {
346: System.err.println("Usage: "
347: + ClassFeatureSet.class.getName()
348: + " <class 1> <class 2>");
349: System.exit(1);
350: }
351:
352: JavaClass a = Repository.lookupClass(args[0]);
353: JavaClass b = Repository.lookupClass(args[1]);
354:
355: ClassFeatureSet aFeatures = new ClassFeatureSet().initialize(a);
356: ClassFeatureSet bFeatures = new ClassFeatureSet().initialize(b);
357:
358: System.out.println("Similarity is "
359: + similarity(aFeatures, bFeatures));
360: System.out.println("Classes are"
361: + (aFeatures.similarTo(bFeatures) ? "" : " not")
362: + " similar");
363: }
364:
365: public static final String ELEMENT_NAME = "ClassFeatureSet";
366: public static final String FEATURE_ELEMENT_NAME = "Feature";
367:
368: /* (non-Javadoc)
369: * @see edu.umd.cs.findbugs.xml.XMLWriteable#writeXML(edu.umd.cs.findbugs.xml.XMLOutput)
370: */
371: public void writeXML(XMLOutput xmlOutput) throws IOException {
372: xmlOutput.openTag(ELEMENT_NAME, new XMLAttributeList()
373: .addAttribute("class", className));
374: for (Iterator<String> i = featureIterator(); i.hasNext();) {
375: String feature = i.next();
376: xmlOutput.openCloseTag(FEATURE_ELEMENT_NAME,
377: new XMLAttributeList().addAttribute("value",
378: feature));
379: }
380: xmlOutput.closeTag(ELEMENT_NAME);
381: }
382: }
|