[Clirr-devel] CVS: clirr/core/src/java/net/sf/clirr/core/internal/checks ClassHierarchyCheck.java,NO
Status: Alpha
Brought to you by:
lkuehne
From: <lk...@us...> - 2004-07-10 13:37:41
|
Update of /cvsroot/clirr/clirr/core/src/java/net/sf/clirr/core/internal/checks In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv23600/src/java/net/sf/clirr/core/internal/checks Added Files: ClassHierarchyCheck.java ClassModifierCheck.java ClassScopeCheck.java FieldSetCheck.java GenderChangeCheck.java InterfaceSetCheck.java MethodSetCheck.java package.html Log Message: moving from root directory to core --- NEW FILE --- ////////////////////////////////////////////////////////////////////////////// // Clirr: compares two versions of a java library for binary compatibility // Copyright (C) 2003 - 2004 Lars Kühne // // 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 net.sf.clirr.core.internal.checks; import net.sf.clirr.core.Message; import net.sf.clirr.core.internal.AbstractDiffReporter; import net.sf.clirr.core.internal.ApiDiffDispatcher; import net.sf.clirr.core.internal.ClassChangeCheck; import net.sf.clirr.core.internal.CoIterator; import net.sf.clirr.core.internal.JavaClassNameComparator; import org.apache.bcel.classfile.JavaClass; /** * Detects changes in the set of superclasses. * * @author lkuehne */ public final class ClassHierarchyCheck extends AbstractDiffReporter implements ClassChangeCheck { private static final Message MSG_ADDED_CLASS_TO_SUPERCLASSES = new Message(5000); private static final Message MSG_REMOVED_CLASS_FROM_SUPERCLASSES = new Message(5001); /** * Create a new instance of this check. * @param dispatcher the diff dispatcher that distributes the detected changes to the listeners. */ public ClassHierarchyCheck(ApiDiffDispatcher dispatcher) { super(dispatcher); } /** {@inheritDoc} */ public boolean check(JavaClass compatBaseline, JavaClass currentVersion) { JavaClass[] compatSupers = compatBaseline.getSuperClasses(); JavaClass[] currentSupers = currentVersion.getSuperClasses(); boolean isThrowable = false; for (int i = 0; i < compatSupers.length; i++) { JavaClass javaClass = compatSupers[i]; if ("java.lang.Throwable".equals(javaClass.getClassName())) { isThrowable = true; } } final String className = compatBaseline.getClassName(); CoIterator iter = new CoIterator(JavaClassNameComparator.COMPARATOR, compatSupers, currentSupers); while (iter.hasNext()) { iter.next(); JavaClass baselineSuper = (JavaClass) iter.getLeft(); JavaClass currentSuper = (JavaClass) iter.getRight(); if (baselineSuper == null) { net.sf.clirr.core.Severity severity; if (isThrowable) { severity = net.sf.clirr.core.Severity.WARNING; } else { severity = net.sf.clirr.core.Severity.INFO; } log(MSG_ADDED_CLASS_TO_SUPERCLASSES, severity, className, null, null, new String[]{currentSuper.getClassName()}); } else if (currentSuper == null) { log(MSG_REMOVED_CLASS_FROM_SUPERCLASSES, net.sf.clirr.core.Severity.ERROR, className, null, null, new String[]{baselineSuper.getClassName()}); } } return true; } } --- NEW FILE --- ////////////////////////////////////////////////////////////////////////////// // Clirr: compares two versions of a java library for binary compatibility // Copyright (C) 2003 - 2004 Lars Kühne // // 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 net.sf.clirr.core.internal.checks; import net.sf.clirr.core.Severity; import net.sf.clirr.core.Message; import net.sf.clirr.core.internal.AbstractDiffReporter; import net.sf.clirr.core.internal.ApiDiffDispatcher; import net.sf.clirr.core.internal.ClassChangeCheck; import net.sf.clirr.core.CheckerException; import net.sf.clirr.core.ScopeSelector; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; /** * Detects changes in class modifiers (abstract, final). * * @author lkuehne */ public final class ClassModifierCheck extends AbstractDiffReporter implements ClassChangeCheck { private static final net.sf.clirr.core.Message MSG_MODIFIER_UNABLE_TO_DETERMINE_CLASS_SCOPE = new Message(3000); private static final net.sf.clirr.core.Message MSG_MODIFIER_REMOVED_FINAL = new Message(3001); private static final net.sf.clirr.core.Message MSG_MODIFIER_ADDED_FINAL_TO_EFFECTIVE_FINAL = new Message(3002); private static final net.sf.clirr.core.Message MSG_MODIFIER_ADDED_FINAL = new Message(3003); private static final net.sf.clirr.core.Message MSG_MODIFIER_REMOVED_ABSTRACT = new Message(3004); private static final net.sf.clirr.core.Message MSG_MODIFIER_ADDED_ABSTRACT = new Message(3005); /** * Create a new instance of this check. * @param dispatcher the diff dispatcher that distributes the detected changes to the listeners. */ public ClassModifierCheck(ApiDiffDispatcher dispatcher) { super(dispatcher); } /** {@inheritDoc} */ public boolean check(JavaClass compatBaseLine, JavaClass currentVersion) { final String className = compatBaseLine.getClassName(); try { net.sf.clirr.core.ScopeSelector.Scope currentScope = net.sf.clirr.core.ScopeSelector.getClassScope(currentVersion); if (currentScope == net.sf.clirr.core.ScopeSelector.SCOPE_PRIVATE) { // for private classes, we don't care if they are now final, // or now abstract, or now an interface. return true; } } catch (CheckerException ex) { log(MSG_MODIFIER_UNABLE_TO_DETERMINE_CLASS_SCOPE, net.sf.clirr.core.Severity.ERROR, className, null, null, null); return true; } final boolean currentIsFinal = currentVersion.isFinal(); final boolean compatIsFinal = compatBaseLine.isFinal(); final boolean currentIsAbstract = currentVersion.isAbstract(); final boolean compatIsAbstract = compatBaseLine.isAbstract(); final boolean currentIsInterface = currentVersion.isInterface(); final boolean compatIsInterface = compatBaseLine.isInterface(); if (compatIsFinal && !currentIsFinal) { log(MSG_MODIFIER_REMOVED_FINAL, net.sf.clirr.core.Severity.INFO, className, null, null, null); } else if (!compatIsFinal && currentIsFinal) { if (isEffectivelyFinal(compatBaseLine)) { log(MSG_MODIFIER_ADDED_FINAL_TO_EFFECTIVE_FINAL, net.sf.clirr.core.Severity.INFO, className, null, null, null); } else { log(MSG_MODIFIER_ADDED_FINAL, net.sf.clirr.core.Severity.ERROR, className, null, null, null); } } // interfaces are always abstract, don't report gender change here if (compatIsAbstract && !currentIsAbstract && !compatIsInterface) { log(MSG_MODIFIER_REMOVED_ABSTRACT, net.sf.clirr.core.Severity.INFO, className, null, null, null); } else if (!compatIsAbstract && currentIsAbstract && !currentIsInterface) { log(MSG_MODIFIER_ADDED_ABSTRACT, net.sf.clirr.core.Severity.ERROR, className, null, null, null); } return true; } /** * There are cases where nonfinal classes are effectively final * because they do not have public or protected ctors. For such * classes we should not emit errors when a final modifier is * introduced. */ private boolean isEffectivelyFinal(JavaClass clazz) { if (clazz.isFinal()) { return true; } // iterate over all constructors, and detect whether any are // public or protected. If so, return false. Method[] methods = clazz.getMethods(); for (int i = 0; i < methods.length; ++i) { Method method = methods[i]; final String methodName = method.getName(); if (methodName.equals("<init>")) { if (method.isPublic() || method.isProtected()) { return false; } } } // no public or protected constructor found return true; } } --- NEW FILE --- ////////////////////////////////////////////////////////////////////////////// // Clirr: compares two versions of a java library for binary compatibility // Copyright (C) 2003 - 2004 Lars Kühne // // 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 net.sf.clirr.core.internal.checks; import net.sf.clirr.core.Severity; import net.sf.clirr.core.ScopeSelector; import net.sf.clirr.core.Message; import net.sf.clirr.core.internal.AbstractDiffReporter; import net.sf.clirr.core.internal.ApiDiffDispatcher; import net.sf.clirr.core.internal.ClassChangeCheck; import net.sf.clirr.core.CheckerException; import org.apache.bcel.classfile.JavaClass; /** * Detects changes in class access declaration, for both "top-level" classes, * and nested classes. * <p> * Java class files only ever contain scope specifiers of "public" or "package". * For top-level classes, this is expected: it is not possible to have a * top-level protected or private class. * <p> * However nested classes <i>can</i> be declared as protected or private. The * way to tell the real scope of a nested class is to ignore the scope in * the actual class file itself, and instead look in the "InnerClasses" * attribute stored on the enclosing class. This is exactly what the java * compiler does when compiling, and what the jvm does when verifying class * linkage at runtime. * * @author Simon Kitching */ public final class ClassScopeCheck extends AbstractDiffReporter implements ClassChangeCheck { private static final net.sf.clirr.core.Message MSG_SCOPE_INCREASED = new Message(1000); private static final net.sf.clirr.core.Message MSG_SCOPE_DECREASED = new Message(1001); private static final net.sf.clirr.core.Message MSG_ERROR_DETERMINING_SCOPE_OLD = new Message(1002); private static final net.sf.clirr.core.Message MSG_ERROR_DETERMINING_SCOPE_NEW = new Message(1003); private net.sf.clirr.core.ScopeSelector scopeSelector; /** * Create a new instance of this check. * @param dispatcher the diff dispatcher that distributes the detected changes to the listeners. */ public ClassScopeCheck(ApiDiffDispatcher dispatcher, net.sf.clirr.core.ScopeSelector scopeSelector) { super(dispatcher); this.scopeSelector = scopeSelector; } /** {@inheritDoc} */ public boolean check(JavaClass compatBaseline, JavaClass currentVersion) { net.sf.clirr.core.ScopeSelector.Scope bScope; try { bScope = net.sf.clirr.core.ScopeSelector.getClassScope(compatBaseline); } catch (CheckerException ex) { log(MSG_ERROR_DETERMINING_SCOPE_OLD, net.sf.clirr.core.Severity.ERROR, compatBaseline.getClassName(), null, null, new String[]{ex.getMessage()}); return false; } net.sf.clirr.core.ScopeSelector.Scope cScope; try { cScope = net.sf.clirr.core.ScopeSelector.getClassScope(currentVersion); } catch (CheckerException ex) { log(MSG_ERROR_DETERMINING_SCOPE_NEW, net.sf.clirr.core.Severity.ERROR, compatBaseline.getClassName(), null, null, new String[]{ex.getMessage()}); return false; } if (!scopeSelector.isSelected(bScope) && !scopeSelector.isSelected(cScope)) { // neither the old nor the new class are "visible" at the scope // the user of this class cares about, so just skip this test // and all following tests for this pair of classes. return false; } if (cScope.isMoreVisibleThan(bScope)) { String[] args = {bScope.getDesc(), cScope.getDesc()}; log(MSG_SCOPE_INCREASED, net.sf.clirr.core.Severity.INFO, compatBaseline.getClassName(), null, null, args); } else if (cScope.isLessVisibleThan(bScope)) { String[] args = {bScope.getDesc(), cScope.getDesc()}; log(MSG_SCOPE_DECREASED, net.sf.clirr.core.Severity.ERROR, compatBaseline.getClassName(), null, null, args); } // Apply further checks only if both versions of the class have scopes // of interest. For example, when the user is only interested in // public & protected classes, then for classes which have just become // public/protected we just want to report that it is now "visible"; // because the class was not visible before the differences since its // last version are not relevant. And for classes which are no longer // public/protected, we just want to report that the whole class is no // longer "visible"; as it is not visible to users any changes to it // are irrelevant. return scopeSelector.isSelected(bScope) && scopeSelector.isSelected(cScope); } } --- NEW FILE --- ////////////////////////////////////////////////////////////////////////////// // Clirr: compares two versions of a java library for binary compatibility // Copyright (C) 2003 - 2004 Lars Kühne // // 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 net.sf.clirr.core.internal.checks; import java.util.Comparator; import net.sf.clirr.core.internal.ClassChangeCheck; import net.sf.clirr.core.internal.AbstractDiffReporter; import net.sf.clirr.core.internal.ApiDiffDispatcher; import net.sf.clirr.core.internal.CoIterator; import net.sf.clirr.core.ApiDifference; import net.sf.clirr.core.Severity; import net.sf.clirr.core.ScopeSelector; import net.sf.clirr.core.Message; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Field; import org.apache.bcel.classfile.ConstantValue; /** * Checks the fields of a class. * * @author lkuehne */ public class FieldSetCheck extends AbstractDiffReporter implements ClassChangeCheck { private static final net.sf.clirr.core.Message MSG_FIELD_ADDED = new Message(6000); private static final net.sf.clirr.core.Message MSG_FIELD_REMOVED = new Message(6001); private static final net.sf.clirr.core.Message MSG_FIELD_NOT_CONSTANT = new Message(6002); private static final net.sf.clirr.core.Message MSG_FIELD_CONSTANT_CHANGED = new Message(6003); private static final net.sf.clirr.core.Message MSG_FIELD_TYPE_CHANGED = new Message(6004); private static final net.sf.clirr.core.Message MSG_FIELD_NOW_NON_FINAL = new Message(6005); private static final net.sf.clirr.core.Message MSG_FIELD_NOW_FINAL = new Message(6006); private static final net.sf.clirr.core.Message MSG_FIELD_NOW_NON_STATIC = new Message(6007); private static final net.sf.clirr.core.Message MSG_FIELD_NOW_STATIC = new Message(6008); private static final net.sf.clirr.core.Message MSG_FIELD_MORE_ACCESSABLE = new Message(6009); private static final net.sf.clirr.core.Message MSG_FIELD_LESS_ACCESSABLE = new Message(6010); private static final class FieldNameComparator implements Comparator { public int compare(Object o1, Object o2) { Field f1 = (Field) o1; Field f2 = (Field) o2; final String name1 = f1.getName(); final String name2 = f2.getName(); return name1.compareTo(name2); } } private static final Comparator COMPARATOR = new FieldNameComparator(); private net.sf.clirr.core.ScopeSelector scopeSelector; public FieldSetCheck(ApiDiffDispatcher dispatcher, net.sf.clirr.core.ScopeSelector scopeSelector) { super(dispatcher); this.scopeSelector = scopeSelector; } public final boolean check(JavaClass baselineClass, JavaClass currentClass) { final Field[] baselineFields = baselineClass.getFields(); final Field[] currentFields = currentClass.getFields(); CoIterator iter = new CoIterator(COMPARATOR, baselineFields, currentFields); while (iter.hasNext()) { iter.next(); Field bField = (Field) iter.getLeft(); Field cField = (Field) iter.getRight(); if (bField == null) { if (scopeSelector.isSelected(cField)) { final String name = cField.getName(); String scope = net.sf.clirr.core.ScopeSelector.getScopeDesc(cField); fireDiff(MSG_FIELD_ADDED, net.sf.clirr.core.Severity.INFO, currentClass, cField, new String[]{scope}); } } else if (cField == null) { if (scopeSelector.isSelected(bField)) { final String name = bField.getName(); fireDiff(MSG_FIELD_REMOVED, net.sf.clirr.core.Severity.ERROR, baselineClass, bField, null); } } else if (scopeSelector.isSelected(bField) || scopeSelector.isSelected(cField)) { checkForModifierChange(bField, cField, currentClass); checkForVisibilityChange(bField, cField, currentClass); checkForTypeChange(bField, cField, currentClass); checkForConstantValueChange(bField, cField, currentClass); } } return true; } private void checkForConstantValueChange(Field bField, Field cField, JavaClass currentClass) { if (!(bField.isStatic() && bField.isFinal() && cField.isStatic() && cField.isFinal())) { return; } final ConstantValue bVal = bField.getConstantValue(); if (bVal != null) { final String bValRep = bVal.toString(); final ConstantValue cVal = cField.getConstantValue(); if (cVal == null) { fireDiff(MSG_FIELD_NOT_CONSTANT, net.sf.clirr.core.Severity.WARNING, currentClass, cField, null); return; } final String cValRep = String.valueOf(cVal); if (!bValRep.equals(cValRep)) { // TODO: print out old and new value // How can that be done with BCEL, esp. for boolean values? fireDiff(MSG_FIELD_CONSTANT_CHANGED, net.sf.clirr.core.Severity.WARNING, currentClass, cField, null); } } } private void checkForTypeChange(Field bField, Field cField, JavaClass currentClass) { final String bSig = bField.getType().toString(); final String cSig = cField.getType().toString(); if (!bSig.equals(cSig)) { fireDiff(MSG_FIELD_TYPE_CHANGED, net.sf.clirr.core.Severity.ERROR, currentClass, bField, new String[]{bSig, cSig}); } } private void checkForModifierChange(Field bField, Field cField, JavaClass clazz) { if (bField.isFinal() && !cField.isFinal()) { fireDiff(MSG_FIELD_NOW_NON_FINAL, net.sf.clirr.core.Severity.INFO, clazz, cField, null); } if (!bField.isFinal() && cField.isFinal()) { fireDiff(MSG_FIELD_NOW_FINAL, net.sf.clirr.core.Severity.ERROR, clazz, cField, null); } if (bField.isStatic() && !cField.isStatic()) { fireDiff(MSG_FIELD_NOW_NON_STATIC, net.sf.clirr.core.Severity.ERROR, clazz, cField, null); } if (!bField.isStatic() && cField.isStatic()) { fireDiff(MSG_FIELD_NOW_STATIC, net.sf.clirr.core.Severity.ERROR, clazz, cField, null); } // JLS, 13.4.10: Adding or deleting a transient modifier of a field // does not break compatibility with pre-existing binaries // TODO: What about volatile? } private void checkForVisibilityChange(Field bField, Field cField, JavaClass clazz) { net.sf.clirr.core.ScopeSelector.Scope bScope = net.sf.clirr.core.ScopeSelector.getScope(bField); net.sf.clirr.core.ScopeSelector.Scope cScope = net.sf.clirr.core.ScopeSelector.getScope(cField); if (cScope.isMoreVisibleThan(bScope)) { fireDiff(MSG_FIELD_MORE_ACCESSABLE, net.sf.clirr.core.Severity.INFO, clazz, cField, new String[]{bScope.getDesc(), cScope.getDesc()}); } else if (cScope.isLessVisibleThan(bScope)) { fireDiff(MSG_FIELD_LESS_ACCESSABLE, net.sf.clirr.core.Severity.ERROR, clazz, cField, new String[]{bScope.getDesc(), cScope.getDesc()}); } } private void fireDiff(net.sf.clirr.core.Message msg, net.sf.clirr.core.Severity severity, JavaClass clazz, Field field, String[] args) { final String className = clazz.getClassName(); final net.sf.clirr.core.ApiDifference diff = new ApiDifference(msg, severity, className, null, field.getName(), args); getApiDiffDispatcher().fireDiff(diff); } } --- NEW FILE --- ////////////////////////////////////////////////////////////////////////////// // Clirr: compares two versions of a java library for binary compatibility // Copyright (C) 2003 - 2004 Lars Kühne // // 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 net.sf.clirr.core.internal.checks; import net.sf.clirr.core.Severity; import net.sf.clirr.core.Message; import net.sf.clirr.core.internal.AbstractDiffReporter; import net.sf.clirr.core.internal.ApiDiffDispatcher; import net.sf.clirr.core.internal.ClassChangeCheck; import org.apache.bcel.classfile.JavaClass; /** * Detects gender changes (a class became an interface or vice versa). * * @author lkuehne */ public final class GenderChangeCheck extends AbstractDiffReporter implements ClassChangeCheck { private static final net.sf.clirr.core.Message MSG_GENDER_CLASS_TO_INTERFACE = new Message(2000); private static final net.sf.clirr.core.Message MSG_GENDER_INTERFACE_TO_CLASS = new Message(2001); /** * Create a new instance of this check. * @param dispatcher the diff dispatcher that distributes the detected changes to the listeners. */ public GenderChangeCheck(ApiDiffDispatcher dispatcher) { super(dispatcher); } /** {@inheritDoc} */ public boolean check(JavaClass baseLine, JavaClass current) { if (baseLine.isClass() && current.isInterface()) { log(MSG_GENDER_CLASS_TO_INTERFACE, net.sf.clirr.core.Severity.ERROR, baseLine.getClassName(), null, null, null); } else if (baseLine.isInterface() && current.isClass()) { log(MSG_GENDER_INTERFACE_TO_CLASS, net.sf.clirr.core.Severity.ERROR, baseLine.getClassName(), null, null, null); } return true; } } --- NEW FILE --- ////////////////////////////////////////////////////////////////////////////// // Clirr: compares two versions of a java library for binary compatibility // Copyright (C) 2003 - 2004 Lars Kühne // // 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 net.sf.clirr.core.internal.checks; import java.util.HashSet; import java.util.Set; import net.sf.clirr.core.Severity; import net.sf.clirr.core.Message; import net.sf.clirr.core.internal.AbstractDiffReporter; import net.sf.clirr.core.internal.ApiDiffDispatcher; import net.sf.clirr.core.internal.ClassChangeCheck; import net.sf.clirr.core.internal.CoIterator; import net.sf.clirr.core.internal.JavaClassNameComparator; import org.apache.bcel.classfile.JavaClass; /** * Detects changes in the set of interfaces implemented by a class. * * @author lkuehne */ public final class InterfaceSetCheck extends AbstractDiffReporter implements ClassChangeCheck { private static final net.sf.clirr.core.Message MSG_IFACE_ADDED = new Message(4000); private static final net.sf.clirr.core.Message MSG_IFACE_REMOVED = new Message(4001); /** * Create a new instance of this check. * @param dispatcher the diff dispatcher that distributes the detected changes to the listeners. */ public InterfaceSetCheck(ApiDiffDispatcher dispatcher) { super(dispatcher); } /** {@inheritDoc} */ public boolean check(JavaClass compatBaseline, JavaClass currentVersion) { JavaClass[] compatInterfaces = compatBaseline.getAllInterfaces(); JavaClass[] currentInterfaces = currentVersion.getAllInterfaces(); // Note: getAllInterfaces might return multiple array entries with the same // interface, so we need to use sets to remove duplicates... Set compat = createClassSet(compatInterfaces); Set current = createClassSet(currentInterfaces); final String className = compatBaseline.getClassName(); CoIterator iter = new CoIterator(JavaClassNameComparator.COMPARATOR, compat, current); while (iter.hasNext()) { iter.next(); JavaClass compatInterface = (JavaClass) iter.getLeft(); JavaClass currentInterface = (JavaClass) iter.getRight(); if (className.equals(compatInterface.getClassName()) || className.equals(currentInterface.getClassName())) { // This occurs because an interface has itself in the set of all interfaces. // We can't just let the test below handle this case because that won't // work when a gender change has occurred. continue; } if (compatInterface == null) { log(MSG_IFACE_ADDED, net.sf.clirr.core.Severity.INFO, className, null, null, new String[]{currentInterface.getClassName()}); } else if (currentInterface == null) { log(MSG_IFACE_REMOVED, net.sf.clirr.core.Severity.ERROR, className, null, null, new String[]{compatInterface.getClassName()}); } } return true; } private Set createClassSet(JavaClass[] classes) { Set current = new HashSet(); for (int i = 0; i < classes.length; i++) { String currentInterface = classes[i].getClassName(); current.add(currentInterface); } return current; } } --- NEW FILE --- ////////////////////////////////////////////////////////////////////////////// // Clirr: compares two versions of a java library for binary compatibility // Copyright (C) 2003 - 2004 Lars Kühne // // 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 net.sf.clirr.core.internal.checks; import net.sf.clirr.core.ApiDifference; import net.sf.clirr.core.Message; import net.sf.clirr.core.internal.AbstractDiffReporter; import net.sf.clirr.core.internal.ApiDiffDispatcher; import net.sf.clirr.core.internal.ClassChangeCheck; import net.sf.clirr.core.internal.CoIterator; import org.apache.bcel.classfile.Attribute; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; import org.apache.bcel.generic.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * Checks the methods of a class. * * @author lkuehne */ public class MethodSetCheck extends AbstractDiffReporter implements ClassChangeCheck { private static final Message MSG_METHOD_NOW_IN_SUPERCLASS = new Message(7000); private static final Message MSG_METHOD_NOW_IN_INTERFACE = new Message(7001); private static final Message MSG_METHOD_REMOVED = new Message(7002); // 7003 unused private static final Message MSG_METHOD_ARGCOUNT_CHANGED = new Message(7004); private static final Message MSG_METHOD_PARAMTYPE_CHANGED = new Message(7005); private static final Message MSG_METHOD_RETURNTYPE_CHANGED = new Message(7006); private static final Message MSG_METHOD_DEPRECATED = new Message(7007); private static final Message MSG_METHOD_UNDEPRECATED = new Message(7008); private static final Message MSG_METHOD_LESS_ACCESSABLE = new Message(7009); private static final Message MSG_METHOD_MORE_ACCESSABLE = new Message(7010); private static final Message MSG_METHOD_ADDED = new Message(7011); private static final Message MSG_METHOD_ADDED_TO_INTERFACE = new Message(7012); private static final Message MSG_ABSTRACT_METHOD_ADDED = new Message(7013); private net.sf.clirr.core.ScopeSelector scopeSelector; /** {@inheritDoc} */ public MethodSetCheck(ApiDiffDispatcher dispatcher, net.sf.clirr.core.ScopeSelector scopeSelector) { super(dispatcher); this.scopeSelector = scopeSelector; } public final boolean check(JavaClass compatBaseline, JavaClass currentVersion) { // Dont't report method problems when gender has changed, as // really the whole API is a pile of crap then - let GenderChange check // do it's job, and that's it if (compatBaseline.isInterface() ^ currentVersion.isInterface()) { return true; } Map bNameToMethod = buildNameToMethodMap(compatBaseline); Map cNameToMethod = buildNameToMethodMap(currentVersion); CoIterator iter = new CoIterator(null, bNameToMethod.keySet(), cNameToMethod.keySet()); while (iter.hasNext()) { iter.next(); String baselineMethodName = (String) iter.getLeft(); String currentMethodName = (String) iter.getRight(); if (baselineMethodName == null) { // a new method name has been added in the new version List currentMethods = (List) cNameToMethod.get(currentMethodName); reportMethodsAdded(currentVersion, currentMethods); } else if (currentMethodName == null) { // all methods with name x have been removed from the old version List baselineMethods = (List) bNameToMethod.get(baselineMethodName); reportMethodsRemoved(compatBaseline, baselineMethods, currentVersion); } else { // assert baselineMethodName equals currentMethodName List baselineMethods = (List) bNameToMethod.get(baselineMethodName); List currentMethods = (List) cNameToMethod.get(currentMethodName); filterSoftMatchedMethods(compatBaseline, baselineMethods, currentVersion, currentMethods); filterChangedMethods(baselineMethodName, compatBaseline, baselineMethods, currentVersion, currentMethods); // if any methods are left, they have no matching method in // the other version, so report as removed or added respectively. if (!baselineMethods.isEmpty()) { reportMethodsRemoved(compatBaseline, baselineMethods, currentVersion); } if (!currentMethods.isEmpty()) { reportMethodsAdded(currentVersion, currentMethods); } } } return true; } /** * Given a list of old and new methods for a particular method name, * find the (old, new) method pairs which have identical argument lists. * <p> * For these: * <ul> * <li>report on changes in accessability, return type, etc * <li>remove from the list * </ul> * * On return from this method, the old and new method lists contain only * methods whose argument lists have changed between versions [or possibly, * methods which have been deleted while one or more new methods of the * same name have been added, depending on how you view it]. All other * situations have been dealt with. * <p> * Note that one or both method lists may be empty on return from * this method. */ private void filterSoftMatchedMethods(JavaClass compatBaseline, List baselineMethods, JavaClass currentVersion, List currentMethods) { for (Iterator bIter = baselineMethods.iterator(); bIter.hasNext();) { Method bMethod = (Method) bIter.next(); for (Iterator cIter = currentMethods.iterator(); cIter.hasNext();) { Method cMethod = (Method) cIter.next(); if (isSoftMatch(bMethod, cMethod)) { check(compatBaseline, bMethod, cMethod); bIter.remove(); cIter.remove(); break; } } } } /** * Two methods are a "soft" match if they have the same name and argument * list. No two methods on the same class are ever a "soft match" for * each other, because the compiler requires distinct parameter lists for * overloaded methods. This also implies that for a given method on an "old" * class version, there are either zero or one "soft matches" on the new * version. * <p> * However a "soft match" is not sufficient to ensure binary compatibility. * A change in the method return type will result in a link error when used * with code compiled against the previous version of the class. * <p> * There may also be other differences between methods that are regarded * as "soft matches": the exceptions thrown, the deprecation status of the * methods, their accessability, etc. */ private boolean isSoftMatch(Method oldMethod, Method newMethod) { String oldName = oldMethod.getName(); String newName = newMethod.getName(); if (!oldName.equals(newName)) { return false; } StringBuffer buf = new StringBuffer(); appendHumanReadableArgTypeList(oldMethod, buf); String oldArgs = buf.toString(); buf.setLength(0); appendHumanReadableArgTypeList(newMethod, buf); String newArgs = buf.toString(); return (oldArgs.equals(newArgs)); } /** * For each method in the baselineMethods list, find the "best match" * in the currentMethods list, report the changes between this method * pair, then remove both methods from the lists. * <p> * On return, at least one of the method lists will be empty. */ private void filterChangedMethods(String methodName, JavaClass compatBaseline, List baselineMethods, JavaClass currentVersion, List currentMethods) { // ok, we now have to deal with the tricky cases, where it is not // immediately obvious which old methods correspond to which new // methods. // // Here we build a similarity table, i.e. for new method i and old // method j we have number that charaterizes how similar the method // signatures are (0 means equal, higher number means more different) while (!baselineMethods.isEmpty() && !currentMethods.isEmpty()) { int[][] similarityTable = buildSimilarityTable(baselineMethods, currentMethods); int min = Integer.MAX_VALUE; int iMin = baselineMethods.size(); int jMin = currentMethods.size(); for (int i = 0; i < baselineMethods.size(); i++) { for (int j = 0; j < currentMethods.size(); j++) { final int tableEntry = similarityTable[i][j]; if (tableEntry < min) { min = tableEntry; iMin = i; jMin = j; } } } Method iMethod = (Method) baselineMethods.remove(iMin); Method jMethod = (Method) currentMethods.remove(jMin); check(compatBaseline, iMethod, jMethod); } } private int[][] buildSimilarityTable(List baselineMethods, List currentMethods) { int[][] similarityTable = new int[baselineMethods.size()][currentMethods.size()]; for (int i = 0; i < baselineMethods.size(); i++) { for (int j = 0; j < currentMethods.size(); j++) { final Method iMethod = (Method) baselineMethods.get(i); final Method jMethod = (Method) currentMethods.get(j); similarityTable[i][j] = distance(iMethod, jMethod); } } return similarityTable; } private int distance(Method m1, Method m2) { final Type[] m1Args = m1.getArgumentTypes(); final Type[] m2Args = m2.getArgumentTypes(); if (m1Args.length != m2Args.length) { return 1000 * Math.abs(m1Args.length - m2Args.length); } int retVal = 0; for (int i = 0; i < m1Args.length; i++) { if (!m1Args[i].toString().equals(m2Args[i].toString())) { retVal += 1; } } return retVal; } /** * Searches the class hierarchy for a method that has a certtain signature. * @param methodSignature the sig we're looking for * @param clazz class where search starts * @return class name of a superclass of clazz, might be null */ private String findSuperClassWithSignature(String methodSignature, JavaClass clazz) { final JavaClass[] superClasses = clazz.getSuperClasses(); for (int i = 0; i < superClasses.length; i++) { JavaClass superClass = superClasses[i]; final Method[] superMethods = superClass.getMethods(); for (int j = 0; j < superMethods.length; j++) { Method superMethod = superMethods[j]; final String superMethodSignature = getMethodId(superClass, superMethod); if (methodSignature.equals(superMethodSignature)) { return superClass.getClassName(); } } } return null; } /** * Searches the class hierarchy for a method that has a certtain signature. * @param methodSignature the sig we're looking for * @param clazz class where search starts * @return class name of a superinterface of clazz, might be null */ private String findSuperInterfaceWithSignature(String methodSignature, JavaClass clazz) { final JavaClass[] superClasses = clazz.getAllInterfaces(); for (int i = 0; i < superClasses.length; i++) { JavaClass superClass = superClasses[i]; final Method[] superMethods = superClass.getMethods(); for (int j = 0; j < superMethods.length; j++) { Method superMethod = superMethods[j]; final String superMethodSignature = getMethodId(superClass, superMethod); if (methodSignature.equals(superMethodSignature)) { return superClass.getClassName(); } } } return null; } /** * Given a list of methods, report each one as being removed. */ private void reportMethodsRemoved(JavaClass baselineClass, List baselineMethods, JavaClass currentClass) { for (Iterator i = baselineMethods.iterator(); i.hasNext();) { Method method = (Method) i.next(); reportMethodRemoved(baselineClass, method, currentClass); } } /** * Report that a method has been removed from a class. * @param oldClass the class where the method was available * @param oldMethod the method that has been removed * @param currentClass the superclass where the method is now available, might be null */ private void reportMethodRemoved(JavaClass oldClass, Method oldMethod, JavaClass currentClass) { if (!scopeSelector.isSelected(oldMethod)) { return; } String methodSignature = getMethodId(oldClass, oldMethod); String superClassName = findSuperClassWithSignature(methodSignature, currentClass); String superInterfaceName = null; if (oldMethod.isAbstract()) { superInterfaceName = findSuperInterfaceWithSignature(methodSignature, currentClass); } if (superClassName != null) { fireDiff(MSG_METHOD_NOW_IN_SUPERCLASS, net.sf.clirr.core.Severity.INFO, oldClass, oldMethod, new String[]{superClassName}); } else if (superInterfaceName != null) { fireDiff(MSG_METHOD_NOW_IN_INTERFACE, net.sf.clirr.core.Severity.INFO, oldClass, oldMethod, new String[]{superInterfaceName}); } else { fireDiff(MSG_METHOD_REMOVED, net.sf.clirr.core.Severity.ERROR, oldClass, oldMethod, null); } } /** * Given a list of methods, report each one as being added. */ private void reportMethodsAdded(JavaClass currentClass, List currentMethods) { for (Iterator i = currentMethods.iterator(); i.hasNext();) { Method method = (Method) i.next(); reportMethodAdded(currentClass, method); } } /** * Report that a method has been added to a class. */ private void reportMethodAdded(JavaClass newClass, Method newMethod) { if (!scopeSelector.isSelected(newMethod)) { return; } if (newClass.isInterface()) { fireDiff(MSG_METHOD_ADDED_TO_INTERFACE, net.sf.clirr.core.Severity.ERROR, newClass, newMethod, null); } else if (newMethod.isAbstract()) { fireDiff(MSG_ABSTRACT_METHOD_ADDED, net.sf.clirr.core.Severity.ERROR, newClass, newMethod, null); } else { fireDiff(MSG_METHOD_ADDED, net.sf.clirr.core.Severity.INFO, newClass, newMethod, null); } } /** * Builds a map from a method name to a List of methods. */ private Map buildNameToMethodMap(JavaClass clazz) { Method[] methods = clazz.getMethods(); Map retVal = new HashMap(); for (int i = 0; i < methods.length; i++) { Method method = methods[i]; final String name = method.getName(); List set = (List) retVal.get(name); if (set == null) { set = new ArrayList(); retVal.put(name, set); } set.add(method); } return retVal; } private void check(JavaClass compatBaseline, Method baselineMethod, Method currentMethod) { if (!scopeSelector.isSelected(baselineMethod) && !scopeSelector.isSelected(currentMethod)) { return; } checkParameterTypes(compatBaseline, baselineMethod, currentMethod); checkReturnType(compatBaseline, baselineMethod, currentMethod); checkDeclaredExceptions(compatBaseline, baselineMethod, currentMethod); checkDeprecated(compatBaseline, baselineMethod, currentMethod); checkVisibility(compatBaseline, baselineMethod, currentMethod); } private void checkParameterTypes(JavaClass compatBaseline, Method baselineMethod, Method currentMethod) { Type[] bArgs = baselineMethod.getArgumentTypes(); Type[] cArgs = currentMethod.getArgumentTypes(); if (bArgs.length != cArgs.length) { fireDiff(MSG_METHOD_ARGCOUNT_CHANGED, net.sf.clirr.core.Severity.ERROR, compatBaseline, baselineMethod, null); return; } //System.out.println("baselineMethod = " + getMethodId(compatBaseline, baselineMethod)); for (int i = 0; i < bArgs.length; i++) { Type bArg = bArgs[i]; Type cArg = cArgs[i]; if (bArg.toString().equals(cArg.toString())) { continue; } // TODO: Check assignability... String[] args = {"" + (i + 1), cArg.toString()}; fireDiff(MSG_METHOD_PARAMTYPE_CHANGED, net.sf.clirr.core.Severity.ERROR, compatBaseline, baselineMethod, args); } } private void checkReturnType(JavaClass compatBaseline, Method baselineMethod, Method currentMethod) { Type bReturnType = baselineMethod.getReturnType(); Type cReturnType = currentMethod.getReturnType(); // TODO: Check assignability. If the new return type is // assignable to the old type, then the code is source-code // compatible even when binary-incompatible. if (!bReturnType.toString().equals(cReturnType.toString())) { fireDiff(MSG_METHOD_RETURNTYPE_CHANGED, net.sf.clirr.core.Severity.ERROR, compatBaseline, baselineMethod, new String[]{cReturnType.toString()}); } } private void checkDeclaredExceptions(JavaClass compatBaseline, Method baselineMethod, Method currentMethod) { // TODO } private void checkDeprecated(JavaClass compatBaseline, Method baselineMethod, Method currentMethod) { boolean bIsDeprecated = isDeprecated(baselineMethod); boolean cIsDeprecated = isDeprecated(currentMethod); if (bIsDeprecated && !cIsDeprecated) { fireDiff(MSG_METHOD_UNDEPRECATED, net.sf.clirr.core.Severity.INFO, compatBaseline, baselineMethod, null); } else if (!bIsDeprecated && cIsDeprecated) { fireDiff(MSG_METHOD_DEPRECATED, net.sf.clirr.core.Severity.INFO, compatBaseline, baselineMethod, null); } } /** * Report changes in the declared accessability of a method * (public/protected/etc). */ private void checkVisibility(JavaClass compatBaseline, Method baselineMethod, Method currentMethod) { net.sf.clirr.core.ScopeSelector.Scope bScope = net.sf.clirr.core.ScopeSelector.getScope(baselineMethod); net.sf.clirr.core.ScopeSelector.Scope cScope = net.sf.clirr.core.ScopeSelector.getScope(currentMethod); if (cScope.isLessVisibleThan(bScope)) { String[] args = {bScope.getDesc(), cScope.getDesc()}; fireDiff(MSG_METHOD_LESS_ACCESSABLE, net.sf.clirr.core.Severity.ERROR, compatBaseline, baselineMethod, args); } else if (cScope.isMoreVisibleThan(bScope)) { String[] args = {bScope.getDesc(), cScope.getDesc()}; fireDiff(MSG_METHOD_MORE_ACCESSABLE, net.sf.clirr.core.Severity.INFO, compatBaseline, baselineMethod, args); } } /** * Creates a human readable String that is similar to the method signature * and identifies the method within a class. * @param clazz the container of the method * @param method the method to identify. * @return a human readable id, for example "public void print(java.lang.String)" */ private String getMethodId(JavaClass clazz, Method method) { StringBuffer buf = new StringBuffer(); final String scopeDecl = net.sf.clirr.core.ScopeSelector.getScopeDecl(method); if (scopeDecl.length() > 0) { buf.append(scopeDecl); buf.append(" "); } String name = method.getName(); if ("<init>".equals(name)) { final String className = clazz.getClassName(); int idx = className.lastIndexOf('.'); name = className.substring(idx + 1); } else { buf.append(method.getReturnType()); buf.append(' '); } buf.append(name); buf.append('('); appendHumanReadableArgTypeList(method, buf); buf.append(')'); return buf.toString(); } private void appendHumanReadableArgTypeList(Method method, StringBuffer buf) { Type[] argTypes = method.getArgumentTypes(); String argSeparator = ""; for (int i = 0; i < argTypes.length; i++) { buf.append(argSeparator); buf.append(argTypes[i].toString()); argSeparator = ", "; } } private void fireDiff(net.sf.clirr.core.Message msg, net.sf.clirr.core.Severity severity, JavaClass clazz, Method method, String[] args) { final String className = clazz.getClassName(); final ApiDifference diff = new ApiDifference(msg, severity, className, getMethodId(clazz, method), null, args); getApiDiffDispatcher().fireDiff(diff); } private boolean isDeprecated(Method method) { Attribute[] attrs = method.getAttributes(); for (int i = 0; i < attrs.length; ++i) { if (attrs[i] instanceof org.apache.bcel.classfile.Deprecated) { return true; } } return false; } } --- NEW FILE --- <html> <body> The check modules that implement the funtionality of clirr. </body> </html> |