blob: f9460a83954f65c7ca9689df7558fb882fb207ee [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* 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.google.gwt.tools.apichecker;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Produces the diff between the API of two apiClasses.
*/
final class ApiClassDiffGenerator implements Comparable<ApiClassDiffGenerator> {
static final Collection<ApiChange> EMPTY_COLLECTION = new ArrayList<ApiChange>(0);
static String printSetWithHashCode(Set<?> set, String identifier) {
StringBuffer sb = new StringBuffer();
sb.append(identifier + ", size = " + set.size());
for (Object element : set) {
sb.append(element + ", hashcode = " + element.hashCode());
}
sb.append("\n");
return sb.toString();
}
// TODO: variable never read, remove?
private final ApiDiffGenerator apiDiffGenerator;
private final String className;
private HashMap<ApiField, Set<ApiChange>> intersectingFields = null;
/**
* Map from methods and constructors in intersection to a string describing
* how they have changed. The description could be the addition/removal of a
* static/abstract/final keyword.
*/
private EnumMap<ApiClass.MethodType, Map<ApiAbstractMethod, Set<ApiChange>>> intersectingMethods;
private Set<ApiField> missingFields = null;
/**
* list of missing constructors and methods.
*/
private EnumMap<ApiClass.MethodType, Set<ApiAbstractMethod>> missingMethods;
private final ApiClass newClass;
private final ApiClass oldClass;
ApiClassDiffGenerator(String className, ApiPackageDiffGenerator apiPackageDiffGenerator)
throws NotFoundException {
this.className = className;
apiDiffGenerator = apiPackageDiffGenerator.getApiDiffGenerator();
this.newClass = apiPackageDiffGenerator.getNewApiPackage().getApiClass(className);
this.oldClass = apiPackageDiffGenerator.getOldApiPackage().getApiClass(className);
if (newClass == null || oldClass == null) {
throw new NotFoundException("for class " + className + ", one of the class objects is null");
}
intersectingFields = new HashMap<ApiField, Set<ApiChange>>();
intersectingMethods =
new EnumMap<ApiClass.MethodType, Map<ApiAbstractMethod, Set<ApiChange>>>(
ApiClass.MethodType.class);
missingMethods =
new EnumMap<ApiClass.MethodType, Set<ApiAbstractMethod>>(ApiClass.MethodType.class);
for (ApiClass.MethodType methodType : ApiClass.MethodType.values()) {
intersectingMethods.put(methodType, new HashMap<ApiAbstractMethod, Set<ApiChange>>());
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(ApiClassDiffGenerator other) {
return getName().compareTo(other.getName());
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ApiClassDiffGenerator)) {
return false;
}
return this.getName().equals(((ApiClassDiffGenerator) o).getName());
}
@Override
public int hashCode() {
return this.getName().hashCode();
}
// TODO(amitmanjhi): handle methods with variable length arguments
void computeApiDiff() {
Set<String> newFieldNames = newClass.getApiFieldNames();
Set<String> oldFieldNames = oldClass.getApiFieldNames();
Set<String> intersection = ApiDiffGenerator.removeIntersection(newFieldNames, oldFieldNames);
missingFields = oldClass.getApiFieldsBySet(oldFieldNames);
processFieldsInIntersection(intersection);
for (ApiClass.MethodType methodType : ApiClass.MethodType.values()) {
Set<String> newMethodNames = newClass.getApiMemberNames(methodType);
Set<String> oldMethodNames = oldClass.getApiMemberNames(methodType);
intersection = ApiDiffGenerator.removeIntersection(newMethodNames, oldMethodNames);
missingMethods.put(methodType, oldClass.getApiMembersBySet(oldMethodNames, methodType));
processElementsInIntersection(intersection, methodType);
}
}
Collection<ApiChange> getApiDiff() {
Collection<ApiChange.Status> apiStatusChanges = oldClass.getModifierChanges(newClass);
Collection<ApiChange> apiChangeCollection = new ArrayList<ApiChange>();
for (ApiChange.Status apiStatus : apiStatusChanges) {
apiChangeCollection.add(new ApiChange(oldClass, apiStatus));
}
// missing fields
for (ApiElement element : missingFields) {
apiChangeCollection.add(new ApiChange(element, ApiChange.Status.MISSING));
}
apiChangeCollection.addAll(getIntersectingFields());
for (ApiClass.MethodType methodType : ApiClass.MethodType.values()) {
apiChangeCollection.addAll(getMissingMethods(methodType));
apiChangeCollection.addAll(getIntersectingMethods(methodType));
}
return apiChangeCollection;
}
String getName() {
return className;
}
/*
* Even though the method name is contained in the "property" parameter, the
* type information is lost. TODO (amitmanjhi): fix this issue later.
*/
private <T> void addProperty(Map<T, Set<ApiChange>> hashMap, T key, ApiChange property) {
Set<ApiChange> value = hashMap.get(key);
if (value == null) {
value = new HashSet<ApiChange>();
}
value.add(property);
hashMap.put(key, value);
}
private Collection<ApiChange> getIntersectingFields() {
Collection<ApiChange> collection = new ArrayList<ApiChange>();
List<ApiField> intersectingFieldsList = new ArrayList<ApiField>(intersectingFields.keySet());
Collections.sort(intersectingFieldsList);
for (ApiField apiField : intersectingFieldsList) {
for (ApiChange apiChange : intersectingFields.get(apiField)) {
collection.add(apiChange);
}
}
return collection;
}
private Collection<ApiChange> getIntersectingMethods(ApiClass.MethodType methodType) {
Collection<ApiChange> collection = new ArrayList<ApiChange>();
List<ApiAbstractMethod> apiMethodsList =
new ArrayList<ApiAbstractMethod>(intersectingMethods.get(methodType).keySet());
Collections.sort(apiMethodsList);
for (ApiAbstractMethod apiMethod : apiMethodsList) {
collection.addAll(intersectingMethods.get(methodType).get(apiMethod));
}
return collection;
}
private Collection<ApiChange> getMissingMethods(ApiClass.MethodType methodType) {
Collection<ApiChange> collection = new ArrayList<ApiChange>();
List<ApiAbstractMethod> apiMethodsList =
new ArrayList<ApiAbstractMethod>(missingMethods.get(methodType));
Collections.sort(apiMethodsList);
for (ApiAbstractMethod apiMethod : apiMethodsList) {
collection.add(new ApiChange(apiMethod, ApiChange.Status.MISSING));
}
return collection;
}
/**
* Attempts to find out if a methodName(null) call previously succeeded, and
* would fail with the new Api. Currently, this method is simple.
* TODO(amitmanjhi): generalize this method.
*
* @param methodsInNew Candidate methods in the new Api
* @param methodsInExisting Candidate methods in the existing Api.
* @return the possible incompatibilities due to method overloading.
*/
private Map<ApiAbstractMethod, ApiChange> getOverloadedMethodIncompatibility(
Set<ApiAbstractMethod> methodsInNew, Set<ApiAbstractMethod> methodsInExisting) {
if (!ApiCompatibilityChecker.API_SOURCE_COMPATIBILITY || methodsInExisting.size() != 1
|| methodsInNew.size() <= 1) {
return Collections.emptyMap();
}
ApiAbstractMethod existingMethod = methodsInExisting.toArray(new ApiAbstractMethod[0])[0];
String signature = existingMethod.getCoarseSignature();
List<ApiAbstractMethod> matchingMethods = new ArrayList<ApiAbstractMethod>();
for (ApiAbstractMethod current : methodsInNew) {
if (current.getCoarseSignature().equals(signature)) {
matchingMethods.add(current);
}
}
if (isPairwiseCompatible(matchingMethods)) {
return Collections.emptyMap();
}
Map<ApiAbstractMethod, ApiChange> incompatibilities =
new HashMap<ApiAbstractMethod, ApiChange>();
incompatibilities.put(existingMethod, new ApiChange(existingMethod,
ApiChange.Status.OVERLOADED_METHOD_CALL,
"Many methods in the new API with similar signatures. Methods = " + methodsInNew
+ " This might break API source compatibility"));
return incompatibilities;
}
/**
* @return true if each pair of methods within the list is compatibile.
*/
private boolean isPairwiseCompatible(List<ApiAbstractMethod> methods) {
int length = methods.size();
for (int i = 0; i < length - 1; i++) {
for (int j = i + 1; j < length; j++) {
ApiAbstractMethod firstMethod = methods.get(i);
ApiAbstractMethod secondMethod = methods.get(j);
if (!firstMethod.isCompatible(secondMethod) && !secondMethod.isCompatible(firstMethod)) {
return false;
}
}
}
return true;
}
/**
* Processes elements in intersection, checking for incompatibilities.
*
* @param intersection
* @param methodType
*/
private void processElementsInIntersection(Set<String> intersection,
ApiClass.MethodType methodType) {
Set<ApiAbstractMethod> missingElements = missingMethods.get(methodType);
Map<ApiAbstractMethod, Set<ApiChange>> intersectingElements =
intersectingMethods.get(methodType);
Set<ApiAbstractMethod> onlyInExisting = new HashSet<ApiAbstractMethod>();
Set<ApiAbstractMethod> onlyInNew = new HashSet<ApiAbstractMethod>();
Set<String> commonSignature = new HashSet<String>();
for (String elementName : intersection) {
Set<ApiAbstractMethod> methodsInNew = newClass.getApiMethodsByName(elementName, methodType);
Set<ApiAbstractMethod> methodsInExisting =
oldClass.getApiMethodsByName(elementName, methodType);
onlyInNew.addAll(methodsInNew);
onlyInExisting.addAll(methodsInExisting);
Map<ApiAbstractMethod, ApiChange> incompatibilityMap =
getOverloadedMethodIncompatibility(methodsInNew, methodsInExisting);
for (Map.Entry<ApiAbstractMethod, ApiChange> entry : incompatibilityMap.entrySet()) {
addProperty(intersectingElements, entry.getKey(), entry.getValue());
}
/*
* We want to find out which method calls that the current API supports
* will succeed even with the new API. Determine this by iterating over
* the methods of the current API. Keep track of a method that has the
* same exact argument types as the old method. If such a method exists,
* check Api compatibility with just that method. Otherwise, check api
* compatibility with ALL methods that might be compatible. (This
* conservative estimate will work as long as we do not change the Api in
* pathological ways.)
*/
for (ApiAbstractMethod methodInExisting : methodsInExisting) {
Set<ApiChange> allPossibleApiChanges = new HashSet<ApiChange>();
ApiAbstractMethod sameSignatureMethod = null;
for (ApiAbstractMethod methodInNew : methodsInNew) {
Set<ApiChange> currentApiChange = new HashSet<ApiChange>();
boolean hasSameSignature = false;
if (methodInExisting.isCompatible(methodInNew)) {
if (methodInExisting.isOverridable()) {
// check if the new method's api is exactly the same
currentApiChange.addAll(methodInExisting.getAllChangesInApi(methodInNew));
} else {
// check for changes to return type and exceptions
currentApiChange.addAll(methodInExisting.checkExceptionsAndReturnType(methodInNew));
}
for (ApiChange.Status status : methodInExisting.getModifierChanges(methodInNew)) {
currentApiChange.add(new ApiChange(methodInExisting, status));
}
if (methodInNew.getInternalSignature().equals(methodInExisting.getInternalSignature())) {
currentApiChange.add(new ApiChange(methodInExisting, ApiChange.Status.COMPATIBLE));
hasSameSignature = true;
} else {
currentApiChange.add(new ApiChange(methodInExisting,
ApiChange.Status.COMPATIBLE_WITH, methodInNew.getApiSignature()));
}
}
if (currentApiChange.size() > 0) {
if (hasSameSignature) {
allPossibleApiChanges = currentApiChange;
sameSignatureMethod = methodInNew;
} else if (sameSignatureMethod == null) {
allPossibleApiChanges.addAll(currentApiChange);
}
}
}
// put the best Api match
if (allPossibleApiChanges.size() > 0) {
onlyInExisting.remove(methodInExisting);
String signatureInExisting = methodInExisting.getInternalSignature();
if (sameSignatureMethod != null
&& signatureInExisting.equals(sameSignatureMethod.getInternalSignature())) {
commonSignature.add(signatureInExisting);
}
for (ApiChange apiChange : allPossibleApiChanges) {
addProperty(intersectingElements, methodInExisting, apiChange);
}
}
}
/**
* Look for incompatiblities that might result due to new methods
* over-loading existing methods. Instead of applying JLS to determine the
* best match, just be conservative and report all possible
* incompatibilities if there is no old method with the exact same
* signature.
*
* <pre>
* class A { // old version
* final void foo(Set<String> p1, Set<String> p2);
* }
*
* class A { // new version
* final void foo(Set<String> p1, Set<String> p2);
* void foo(HashSet<String> p1, Set<String> p2) throws ...;
* }
* </pre>
*/
for (ApiAbstractMethod methodInNew : methodsInNew) {
ApiAbstractMethod sameSignatureMethod = null;
for (ApiAbstractMethod methodInExisting : methodsInExisting) {
if (methodInNew.getInternalSignature().equals(methodInExisting.getInternalSignature())) {
sameSignatureMethod = methodInExisting;
break;
}
}
// do not look for incompatibilities with overloaded methods, if exact
// match exists.
if (sameSignatureMethod != null) {
continue;
}
for (ApiAbstractMethod methodInExisting : methodsInExisting) {
if (methodInNew.isCompatible(methodInExisting)) {
// new method is going to be called instead of existing method,
// determine incompatibilities
for (ApiChange apiChange : methodInExisting.checkExceptionsAndReturnType(methodInNew)) {
addProperty(intersectingElements, methodInExisting, apiChange);
}
}
}
}
// printOutput(commonSignature, onlyInExisting, onlyInNew);
}
missingElements.addAll(onlyInExisting);
}
private void processFieldsInIntersection(Set<String> intersection) {
for (String fieldName : intersection) {
ApiField newField = newClass.getApiFieldByName(fieldName);
ApiField oldField = oldClass.getApiFieldByName(fieldName);
Set<ApiChange> apiChanges = oldField.getModifierChanges(newField);
if (apiChanges.size() > 0) {
intersectingFields.put(oldField, apiChanges);
}
}
}
}