blob: bd74cfb3e0ad6529372945a2deb9b84c653de339 [file] [log] [blame]
/* Copyright (c) 2002-2008 Sun Microsystems, Inc. All rights reserved
*
* This program is distributed under the terms of
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/
package org.pantsbuild.jmake;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Set;
/**
* This class implements checking of source compatibility of classes and supporting operations
*
* @author Misha Dmitriev
* 12 March 2004
*/
public class CompatibilityChecker {
private PCDManager pcdm;
private RefClassFinder rf;
ClassInfo oldClassInfo = null;
ClassInfo newClassInfo = null;
private boolean versionsCompatible;
private boolean publicConstantChanged;
public CompatibilityChecker(PCDManager pcdm, boolean failOnDependentJar, boolean noWarnOnDependentJar) {
this.pcdm = pcdm;
publicConstantChanged = false;
rf = new RefClassFinder(pcdm, failOnDependentJar, noWarnOnDependentJar);
}
/**
* Compares the two class versions for the given PCDEntry. Returns true if all changes are source
* compatible, and false otherwise.
*/
public boolean compareClassVersions(PCDEntry entry) {
// I once had the following optimization here with the comment "No sense to make any further checks if
// everything is recompiled anyway", but now I believe it's wrong. For each class that was found changed
// we need to know whether the new version is compatible with the old or not, since this may determine
// whether the new version of this class is promoted into the pdb or not (see PCDManager.updateClassInfoInPCD()).
// So, all changed classes should be checked just to correctly determine version compatibility.
// if (publicConstantChanged) return false;
oldClassInfo = pcdm.getClassInfoForPCDEntry(ClassInfo.VER_OLD, entry);
newClassInfo = pcdm.getClassInfoForPCDEntry(ClassInfo.VER_NEW, entry);
rf.initialize(oldClassInfo.name, entry.javaFileFullPath.endsWith(".jar"));
versionsCompatible = true;
checkAccessFlags();
checkSuperclasses();
checkImplementedInterfaces();
checkFields();
checkMethodsAndConstructors();
return versionsCompatible;
}
/** Find all dependent classes for a deleted class. */
public void checkDeletedClass(PCDEntry entry) {
oldClassInfo = entry.oldClassInfo;
rf.initialize(oldClassInfo.name, entry.javaFileFullPath.endsWith(".jar"));
rf.findReferencingClassesForDeletedClass(oldClassInfo);
// It may happen that the only reference to deleted class X is via "X.class" construct
String packageToLookIn =
oldClassInfo.isPublic() ? null : oldClassInfo.packageName;
rf.findClassesDeclaringField(("class$" + oldClassInfo.name).intern(), "java/lang/Class", true, packageToLookIn);
checkForFinalFields();
}
/** Returns the names of classes affected by source incompatible changes to the new version of the checked class. */
public String[] getAffectedClasses() {
return rf.getAffectedClassNames();
}
/** All of the following methods return true if no source incompatible changes found, and false otherwise */
private void checkAccessFlags() {
char oldClassFlags = oldClassInfo.accessFlags;
char newClassFlags = newClassInfo.accessFlags;
if (oldClassFlags == newClassFlags) {
return;
}
if (!Modifier.isFinal(oldClassFlags) && Modifier.isFinal(newClassFlags)) {
versionsCompatible = false;
rf.findDirectSubclasses(oldClassInfo);
}
if (!Modifier.isAbstract(oldClassFlags) && Modifier.isAbstract(newClassFlags)) {
versionsCompatible = false;
rf.findReferencingClasses0(oldClassInfo);
}
// Now to accessibility modifiers checking...
if (Modifier.isPublic(newClassFlags)) {
return;
}
if (Modifier.isProtected(newClassFlags)) {
if (Modifier.isPublic(oldClassFlags)) {
versionsCompatible = false;
rf.findDiffPackageAndNotSubReferencingClasses1(oldClassInfo);
}
} else if (Modifier.isPrivate(newClassFlags)) {
if (!Modifier.isPrivate(oldClassFlags)) {
versionsCompatible = false;
} else {
return; // private -> private, nothing more to check
}
if (Modifier.isPublic(oldClassFlags)) {
rf.findReferencingClasses1(oldClassInfo);
} else if (Modifier.isProtected(oldClassFlags)) {
rf.findThisPackageOrSubReferencingClasses1(oldClassInfo);
} else {
rf.findThisPackageReferencingClasses1(oldClassInfo);
}
} else { // newClassFlags has default access, since public has already been excluded
if (Modifier.isPublic(oldClassFlags)) {
versionsCompatible = false;
rf.findDiffPackageReferencingClasses1(oldClassInfo);
} else if (Modifier.isProtected(oldClassFlags)) {
versionsCompatible = false;
rf.findDiffPackageAndSubReferencingClasses1(oldClassInfo);
}
}
}
private void checkSuperclasses() {
List<String> oldSuperNames = oldClassInfo.getAllSuperclassNames();
List<String> newSuperNames = newClassInfo.getAllSuperclassNames();
int oldNamesSizeMinusOne = oldSuperNames.size() - 1;
for (int i = 0; i <= oldNamesSizeMinusOne; i++) {
String oldSuperName = oldSuperNames.get(i);
if (!newSuperNames.contains(oldSuperName)) {
versionsCompatible = false;
ClassInfo missingSuperClass =
pcdm.getClassInfoForName(ClassInfo.VER_OLD, oldSuperName);
if (missingSuperClass == null) { // This class is not in project
missingSuperClass =
ClassPath.getClassInfoForName(oldSuperName, pcdm);
if (missingSuperClass == null) {
missingSuperClass = new ClassInfo(oldSuperName, pcdm);
}
}
rf.findReferencingClasses2(missingSuperClass, oldClassInfo);
}
}
// Now check if the class is an exception, and its kind has changed from unchecked to checked
if (oldClassInfo.isInterface() || oldSuperNames.size() == 0) {
return;
}
if (!(oldSuperNames.contains("java/lang/RuntimeException") || oldSuperNames.contains("java/lang/Error"))) {
return;
}
if (!(newSuperNames.contains("java/lang/RuntimeException") || newSuperNames.contains("java/lang/Error"))) {
if (!newSuperNames.contains("java/lang/Throwable")) {
return;
}
// Ok, exception kind has changed from unchecked to checked.
versionsCompatible = false;
rf.findReferencingClasses0(oldClassInfo);
rf.findRefsToMethodsThrowingException(oldClassInfo);
}
}
private void checkImplementedInterfaces() {
Set<String> oldIntfNames = oldClassInfo.getAllImplementedIntfNames();
Set<String> newIntfNames = newClassInfo.getAllImplementedIntfNames();
for (String oldIntfName : oldIntfNames) {
if (!newIntfNames.contains(oldIntfName)) {
versionsCompatible = false;
ClassInfo missingSuperInterface =
pcdm.getClassInfoForName(ClassInfo.VER_OLD, oldIntfName);
if (missingSuperInterface == null) { // This class is not in project
missingSuperInterface =
ClassPath.getClassInfoForName(oldIntfName, pcdm);
if (missingSuperInterface == null) {
missingSuperInterface = new ClassInfo(oldIntfName, pcdm);
}
}
rf.findReferencingClasses2(missingSuperInterface, oldClassInfo);
}
}
// Check if the class is abstract, and an interface has been added to its list of implemented interfaces
if (newClassInfo.isAbstract()) {
for (String newIntfName : newIntfNames) {
if (!oldIntfNames.contains(newIntfName)) {
versionsCompatible = false;
rf.findConcreteSubclasses(oldClassInfo);
break;
}
}
}
}
private void checkFields() {
String oFNames[] = oldClassInfo.fieldNames;
String oFSignatures[] = oldClassInfo.fieldSignatures;
char oFFlags[] = oldClassInfo.fieldAccessFlags;
String nFNames[] = newClassInfo.fieldNames;
String nFSignatures[] = newClassInfo.fieldSignatures;
char nFFlags[] = newClassInfo.fieldAccessFlags;
int oFLen = oFNames != null ? oFNames.length : 0;
int nFLen = nFNames != null ? nFNames.length : 0;
int oFMod, nFMod;
String oFName, oFSig, nFName;
int i, j, k, endIdx;
int nonMatchingNewFields = nFLen;
for (i = 0; i < oFLen; i++) {
oFMod = oFFlags[i];
if (Modifier.isPrivate(oFMod)) {
continue; // Changes to private fields don't affect compatibility
}
oFName = oFNames[i];
oFSig = oFSignatures[i];
boolean found = false;
// Look for the same field in the new version considering name and type
endIdx = nFLen - 1;
k = i < nFLen ? i : endIdx;
for (j = 0; j < nFLen; j++) {
if (oFName.equals(nFNames[k]) &&
oFSig.equals(nFSignatures[k])) {
found = true;
break;
}
if (k < endIdx) {
k++;
} else {
k = 0;
}
}
if (found) {
nonMatchingNewFields--;
nFMod = nFFlags[k];
checkFieldModifiers(oFMod, nFMod, i, k);
if (publicConstantChanged) {
return;
}
} else { // Matching field not found
if (Modifier.isStatic(oFMod) && Modifier.isFinal(oFMod) &&
oldClassInfo.primitiveConstantInitValues != null &&
oldClassInfo.primitiveConstantInitValues[i] != null) {
// Compile-time constant deleted
versionsCompatible = false;
rf.findAllProjectClasses(oldClassInfo, i);
if (Modifier.isPublic(oFMod)) {
publicConstantChanged = true;
return;
}
} else {
versionsCompatible = false;
rf.findReferencingClassesForField(oldClassInfo, i);
}
}
}
if (nonMatchingNewFields > 0) { // There are some fields declared in the new version which don't exist in the old one
// Look for fields hiding same-named fields in superclasses
for (i = 0; i < nFLen; i++) {
nFName = nFNames[i];
boolean found = false;
for (j = 0; j < oFLen; j++) {
if (nFName.equals(oFNames[j])) {
found = true;
break;
}
}
if (found) {
continue; // nFName is not an added field
}
String superName = oldClassInfo.superName;
ClassInfo superInfo;
while (superName != null) {
superInfo =
pcdm.getClassInfoForName(ClassInfo.VER_OLD, superName);
if (superInfo == null) {
break;
}
String[] superOFNames = superInfo.fieldNames;
int superOFNamesLen = superOFNames != null ? superOFNames.length
: 0;
for (j = 0; j < superOFNamesLen; j++) {
if (nFName == superOFNames[j]) {
versionsCompatible = false;
rf.findReferencingClassesForField(superInfo, j);
}
}
superName = superInfo.superName;
}
}
}
}
/** It is already known that old field is not private */
private void checkFieldModifiers(int oFMod, int nFMod, int oldFieldIdx, int newFieldIdx) {
if (oFMod == nFMod) {
if (Modifier.isFinal(oFMod) &&
(!ClassInfo.constFieldInitValuesEqual(oldClassInfo, oldFieldIdx, newClassInfo, newFieldIdx))) {
versionsCompatible = false;
rf.findAllProjectClasses(oldClassInfo, oldFieldIdx);
if (Modifier.isPublic(oFMod)) {
publicConstantChanged = true; // Means we will have to recompile ALL project classes
}
return;
}
}
// These tests are ordered such that if a previous test succeeds, there is no need to do further tests, since that
// former test will cause more classes to be checked than any of the further tests. That is why it is possible to
// check properties that are in fact independent (e.g. accessibility vs. static/non-static) together. But this
// optimization only works since all kinds of tests result in the same kind of find..ReferencingClassesForField()
// outcome. For methods this is not true, and so there we have to check independent properties separately.
if (Modifier.isStatic(oFMod) && Modifier.isFinal(oFMod) && // oFMod is known to be non-private
(!Modifier.isFinal(nFMod) || !ClassInfo.constFieldInitValuesEqual(oldClassInfo, oldFieldIdx, newClassInfo, newFieldIdx))) {
versionsCompatible = false;
rf.findAllProjectClasses(oldClassInfo, oldFieldIdx);
if (Modifier.isPublic(oFMod)) {
publicConstantChanged = true;
}
} else if (Modifier.isPrivate(nFMod) || // oFMod is known to be non-private
(!Modifier.isFinal(oFMod) && Modifier.isFinal(nFMod)) ||
(Modifier.isStatic(oFMod) != Modifier.isStatic(nFMod)) ||
(Modifier.isVolatile(oFMod) != Modifier.isVolatile(nFMod))) {
versionsCompatible = false;
rf.findReferencingClassesForField(oldClassInfo, oldFieldIdx);
} else if (Modifier.isPublic(oFMod) && Modifier.isProtected(nFMod)) {
versionsCompatible = false;
rf.findDiffPackageReferencingClassesForField(oldClassInfo, oldFieldIdx);
} else if ((Modifier.isPublic(oFMod) || Modifier.isProtected(oFMod)) &&
(!(Modifier.isPublic(nFMod) || Modifier.isProtected(nFMod) || Modifier.isPrivate(nFMod)))) {
versionsCompatible = false;
if (Modifier.isPublic(oFMod)) {
rf.findDiffPackageReferencingClassesForField(oldClassInfo, oldFieldIdx);
} else {
rf.findDiffPackageAndSubReferencingClassesForField(oldClassInfo, oldFieldIdx);
}
}
}
private void checkForFinalFields() {
char oFFlags[] = oldClassInfo.fieldAccessFlags;
int oFLen = oldClassInfo.fieldNames != null ? oldClassInfo.fieldNames.length
: 0;
int oFMod;
for (int i = 0; i < oFLen; i++) {
oFMod = oFFlags[i];
if (Modifier.isPrivate(oFMod)) {
continue; // Changes to private fields don't affect compatibility
}
if (Modifier.isStatic(oFMod) && Modifier.isFinal(oFMod)) {
rf.findAllProjectClasses(oldClassInfo, i);
if (Modifier.isPublic(oFMod)) {
publicConstantChanged = true;
return;
}
}
}
}
private void checkMethodsAndConstructors() {
String oMNames[] = oldClassInfo.methodNames;
String oMSignatures[] = oldClassInfo.methodSignatures;
char oMFlags[] = oldClassInfo.methodAccessFlags;
String nMNames[] = newClassInfo.methodNames;
String nMSignatures[] = newClassInfo.methodSignatures;
char nMFlags[] = newClassInfo.methodAccessFlags;
int oMLen = oMNames != null ? oMNames.length : 0;
int nMLen = nMNames != null ? nMNames.length : 0;
int oMMod, nMMod;
String oMName, oMSig, nMName, nMSig;
int i, j, k, endIdx;
int nonMatchingNewMethods = nMLen;
for (i = 0; i < oMLen; i++) {
oMMod = oMFlags[i];
if (Modifier.isPrivate(oMMod)) {
continue; // Changes to private methods don't affect compatibility
}
oMName = oMNames[i];
oMSig = oMSignatures[i];
boolean found = false;
// Look for the same method in the new version considering name and signature
endIdx = nMLen - 1;
k = i < nMLen ? i : endIdx;
for (j = 0; j < nMLen; j++) {
if (oMName == nMNames[k] && oMSig == nMSignatures[k]) {
found = true;
break;
}
if (k < endIdx) {
k++;
} else {
k = 0;
}
}
if (found) {
nonMatchingNewMethods--;
nMMod = nMFlags[k];
if (oMMod != nMMod) {
checkMethodModifiers(oMMod, nMMod, i);
}
// Check if the new method throws more exceptions than the old one
if (newClassInfo.checkedExceptions != null && newClassInfo.checkedExceptions[k] != null) {
if (oldClassInfo.checkedExceptions == null) {
versionsCompatible = false;
rf.findReferencingClassesForMethod(oldClassInfo, i);
} else if (oldClassInfo.checkedExceptions[i] == null) {
versionsCompatible = false;
rf.findReferencingClassesForMethod(oldClassInfo, i);
} else {
String oldExceptions[] =
oldClassInfo.checkedExceptions[i];
String newExceptions[] =
newClassInfo.checkedExceptions[k];
for (int ei = 0; ei < newExceptions.length; ei++) {
String newEx = newExceptions[ei];
found = false;
for (int ej = 0; ej < oldExceptions.length; ej++) {
if (newEx.equals(oldExceptions[ej])) {
found = true;
break;
}
}
if (!found) {
versionsCompatible = false;
rf.findReferencingClassesForMethod(oldClassInfo, i);
break;
}
}
}
}
} else { // Matching method not found
versionsCompatible = false;
rf.findReferencingClassesForMethod(oldClassInfo, i);
// Deleting a concrete method from an abstract class is a special case
if (oldClassInfo.isAbstract() && !Modifier.isAbstract(oMMod)) {
rf.findConcreteSubclassesNotOverridingAbstractMethod(oldClassInfo, oldClassInfo, i);
}
}
}
if (nonMatchingNewMethods > 0) { // There are some methods/constructors declared in the new version which don't exist in the old one
if (!oldClassInfo.isInterface()) {
for (i = 0; i < nMLen; i++) {
nMMod = nMFlags[i];
if (Modifier.isPrivate(nMMod)) {
continue;
}
String newMName = nMNames[i];
final String newMSig = nMSignatures[i];
final boolean isStatic = Modifier.isStatic(nMMod);
boolean found = false;
for (j = 0; j < oMLen; j++) {
if (newMName.equals(oMNames[j]) &&
newMSig.equals(oMSignatures[j])) {
found = true;
break;
}
}
if (found) {
continue; // nMName is not an added method
}
// Check if the new method is a static one that hides an inherited static method
// Check if the new method overloads an existing (declared or inherited) method. Overloading test is rough -
// we just check if the number of parameters is the same. Note that if a new constructor has been added, it
// can be treated in the same way, except that we shouldn't look up "same name methods" for it in superclasses.
oldClassInfo.findExistingSameNameMethods(newMName,
!newMName.equals("<init>"), false,
new ClassInfo.MethodHandler() {
void handleMethod(ClassInfo classInfo, int methodIdx) {
String otherMSig =
classInfo.methodSignatures[methodIdx];
if ((newMSig.equals(otherMSig) && isStatic &&
classInfo != oldClassInfo) ||
(newMSig != otherMSig &&
Utils.sameParamNumber(newMSig, otherMSig))) {
versionsCompatible = false;
rf.findReferencingClassesForMethod(classInfo, methodIdx);
}
}
});
if (Modifier.isAbstract(nMMod)) {
// An abstract method added to the class. Find any concrete subclasses that don't override
// or inherit a concrete implementation of this method.
versionsCompatible = false;
rf.findConcreteSubclassesNotOverridingAbstractMethod(oldClassInfo, newClassInfo, i);
}
// Check if there is a method with the same name in some subclass, such that it now overrides
// or overloads the added method.
if (subclassesDeclareSameNameMethod(oldClassInfo, newMName)) {
versionsCompatible = false;
}
}
} else { // We are checking an interface.
for (i = 0; i < nMLen; i++) {
String newMName = nMNames[i];
final String newMSig = nMSignatures[i];
boolean found = false;
for (j = 0; j < oMLen; j++) {
if (newMName == oMNames[j] && newMSig == oMSignatures[j]) {
found = true;
break;
}
}
if (!found) {
versionsCompatible = false;
// Check if the new method overloads an existing (declared or inherited) method. Overloading test is rough -
// we just check if the number of parameters is the same.
oldClassInfo.findExistingSameNameMethods(newMName, true, true, new ClassInfo.MethodHandler() {
void handleMethod(ClassInfo classInfo, int methodIdx) {
String otherMSig =
classInfo.methodSignatures[methodIdx];
if (newMSig != otherMSig &&
Utils.sameParamNumber(newMSig, otherMSig)) {
rf.findReferencingClassesForMethod(classInfo, methodIdx);
}
}
});
rf.findDirectlyAndOtherwiseImplementingConcreteClasses(oldClassInfo);
rf.findAbstractSubtypesWithSameNameMethod(oldClassInfo, newMName, newMSig);
break;
}
}
}
}
}
private void checkMethodModifiers(int oMMod, int nMMod, int oldMethodIdx) {
if (Modifier.isPrivate(nMMod)) {
versionsCompatible = false;
rf.findReferencingClassesForMethod(oldClassInfo, oldMethodIdx);
} else if (Modifier.isPublic(oMMod) && Modifier.isProtected(nMMod)) {
versionsCompatible = false;
rf.findDiffPackageReferencingClassesForMethod(oldClassInfo, oldMethodIdx);
} else if ((Modifier.isPublic(oMMod) || Modifier.isProtected(oMMod)) &&
(!(Modifier.isPublic(nMMod) || Modifier.isProtected(nMMod) || Modifier.isPrivate(nMMod)))) {
versionsCompatible = false;
if (Modifier.isPublic(oMMod)) {
rf.findDiffPackageReferencingClassesForMethod(oldClassInfo, oldMethodIdx);
} else {
rf.findDiffPackageAndSubReferencingClassesForMethod(oldClassInfo, oldMethodIdx);
}
} else if ((Modifier.isPrivate(oMMod) && !Modifier.isPrivate(nMMod)) ||
(Modifier.isProtected(oMMod) && Modifier.isPublic(nMMod)) ||
(!(Modifier.isPublic(oMMod) || Modifier.isProtected(oMMod) || Modifier.isPrivate(oMMod)) &&
(Modifier.isPublic(nMMod) || Modifier.isProtected(nMMod)))) {
versionsCompatible = false;
rf.findSubclassesReimplementingMethod(oldClassInfo, oldMethodIdx);
}
if ((!Modifier.isAbstract(oMMod) && Modifier.isAbstract(nMMod)) ||
(Modifier.isStatic(oMMod) != Modifier.isStatic(nMMod))) {
versionsCompatible = false;
rf.findReferencingClassesForMethod(oldClassInfo, oldMethodIdx);
if (!Modifier.isAbstract(oMMod) && Modifier.isAbstract(nMMod)) {
rf.findConcreteSubclassesNotOverridingAbstractMethod(oldClassInfo, newClassInfo, oldMethodIdx);
}
}
if (!Modifier.isFinal(oMMod) && Modifier.isFinal(nMMod)) {
versionsCompatible = false;
rf.findSubclassesReimplementingMethod(oldClassInfo, oldMethodIdx);
}
}
/**
* Returns true if any subclass(es), direct or indirect, declare a method with name methodName.
* For each such occurence, referencing classes are looked up and added to the list of affected classes.
*/
private boolean subclassesDeclareSameNameMethod(ClassInfo oldClassInfo, String methodName) {
boolean res = false;
ClassInfo[] directSubclasses = oldClassInfo.getDirectSubclasses();
for (int i = 0; i < directSubclasses.length; i++) {
ClassInfo subclass = directSubclasses[i];
int methNo = subclass.declaresSameNameMethod(methodName);
if (methNo >= 0) {
rf.addToAffectedClassNames(subclass.name);
rf.findReferencingClassesForMethod(subclass, methNo);
res = true;
}
if (subclassesDeclareSameNameMethod(subclass, methodName)) {
res = true;
}
}
return res;
}
}