/* * 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); } } } }