blob: 737044a3736fa1a30ea8c29d45074e4b737d6e28 [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.JClassType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* {@link ApiDiffGenerator} encapsulates a class that produces the diff between
* two api's.
*/
public final class ApiDiffGenerator {
private static enum Relation {
NONE, SUBCLASS, SUPERCLASS;
}
public static final String DELIMITER = " ";
/* variable just for debugging -- find why an apiChange is being removed */
static final String HAY_API_CHANGE = "";
/**
* The two types might belong to different typeOracles.
*/
static boolean isFirstTypeAssignableToSecond(JType firstType, JType secondType) {
// getJNISignature() does TypeErasure
if (firstType.getJNISignature().equals(secondType.getJNISignature())) {
return true;
}
JClassType classType1 = firstType.isClassOrInterface();
JClassType classType2 = secondType.isClassOrInterface();
if (classType1 == null || classType2 == null) {
return false;
}
TypeOracle newApiTypeOracle = classType2.getOracle();
// get the appropriate classObject in the newApi
JClassType firstClassType = newApiTypeOracle.findType(classType1.getQualifiedSourceName());
JClassType secondClassType = newApiTypeOracle.findType(classType2.getQualifiedSourceName());
// The types might not necessarily exist in the newApi
if (firstClassType == null || secondClassType == null) {
return false;
}
return firstClassType.isAssignableTo(secondClassType);
}
static Set<String> removeIntersection(Set<String> s1, Set<String> s2) {
Set<String> intersection = new HashSet<String>(s1);
intersection.retainAll(s2);
s1.removeAll(intersection);
s2.removeAll(intersection);
return intersection;
}
Map<String, ApiPackageDiffGenerator> intersectingPackages =
new HashMap<String, ApiPackageDiffGenerator>();
Set<String> missingPackageNames;
final ApiContainer newApi;
final ApiContainer oldApi;
ApiDiffGenerator(ApiContainer newApi, ApiContainer oldApi) {
this.newApi = newApi;
this.oldApi = oldApi;
}
ApiClassDiffGenerator findApiClassDiffGenerator(String className) {
int i = className.length() - 1;
while (i >= 0) {
int dot = className.lastIndexOf('.', i);
String pkgName = "";
String typeName = className;
if (dot != -1) {
pkgName = className.substring(0, dot);
typeName = className.substring(dot + 1);
i = dot - 1;
} else {
i = -1;
}
ApiClassDiffGenerator result = findApiClassDiffGenerator(pkgName, typeName);
if (result != null) {
return result;
}
}
return null;
}
// TODO(amitmanjhi): cache the results
/**
* Finds a type given its package-relative name. For nested classes, use its
* source name rather than its binary name (that is, use a "." rather than a
* "$").
*
* @return <code>null</code> if the type is not found
*/
ApiClassDiffGenerator findApiClassDiffGenerator(String pkgName, String typeName) {
ApiPackageDiffGenerator pkg = findApiPackageDiffGenerator(pkgName);
if (pkg != null) {
ApiClassDiffGenerator type = pkg.findApiClassDiffGenerator(pkgName + "." + typeName);
if (type != null) {
return type;
}
}
return null;
}
ApiPackageDiffGenerator findApiPackageDiffGenerator(String key) {
return intersectingPackages.get(key);
}
Collection<ApiChange> getApiDiff() throws NotFoundException {
computeApiDiff();
Collection<ApiChange> collection = new ArrayList<ApiChange>();
Set<ApiPackage> missingPackages = oldApi.getApiPackagesBySet(missingPackageNames);
for (ApiPackage missingPackage : missingPackages) {
collection.add(new ApiChange(missingPackage, ApiChange.Status.MISSING));
}
for (ApiPackageDiffGenerator intersectingPackage : intersectingPackages.values()) {
collection.addAll(intersectingPackage.getApiDiff());
}
return collection;
}
ApiContainer getNewApiContainer() {
return newApi;
}
ApiContainer getOldApiContainer() {
return oldApi;
}
/**
* Remove any apiChange x if there is another apiChange y such that the
* apiElement and status for both x and y are the same and the apiClass for x
* is a subclass of the apiClass for y.
*
* @param originalCollection collection with duplicates.
* @return collection minus duplicates.
*/
Collection<ApiChange> removeDuplicates(Collection<ApiChange> originalCollection) {
/*
* Map from the hashCode of an apiChange to the list of ApiChanges. There
* can be multiple ApiChanges that have the same hashCode, but neither is a
* subset of another. Example: if B and C both extend A, and there is an
* ApiChange in B and C due to an api element of A.
*/
Map<Integer, Collection<ApiChange>> apiChangeMap =
new HashMap<Integer, Collection<ApiChange>>();
for (ApiChange apiChange : originalCollection) {
String apiChangeStr = apiChange.getApiElement().getRelativeSignature();
Collection<ApiChange> apiChangesSameHashCode =
apiChangeMap.get(apiChange.hashCodeForDuplication());
if (apiChangesSameHashCode == null) {
apiChangesSameHashCode = new HashSet<ApiChange>();
apiChangeMap.put(apiChange.hashCodeForDuplication(), apiChangesSameHashCode);
}
Collection<ApiChange> apiChangesToRemove = new HashSet<ApiChange>();
boolean addNewElement = true;
for (ApiChange oldApiChange : apiChangesSameHashCode) {
String oldApiChangeStr = oldApiChange.getApiElement().getRelativeSignature();
Relation relation =
getRelationOfApiClassOfFirstArgToThatOfSecond(apiChange.getApiElement(), oldApiChange
.getApiElement());
if (relation == Relation.SUPERCLASS) {
apiChangesToRemove.add(oldApiChange);
if (ApiCompatibilityChecker.DEBUG_DUPLICATE_REMOVAL
&& oldApiChangeStr.indexOf(HAY_API_CHANGE) != -1) {
System.out.println(oldApiChangeStr + " replaced by " + apiChangeStr + ", status = "
+ oldApiChange.getStatus());
}
} else if (relation == Relation.SUBCLASS) {
addNewElement = false;
if (ApiCompatibilityChecker.DEBUG_DUPLICATE_REMOVAL
&& apiChangeStr.indexOf(HAY_API_CHANGE) != -1) {
System.out.println(apiChangeStr + " replaced by " + oldApiChangeStr);
}
}
}
apiChangesSameHashCode.removeAll(apiChangesToRemove);
if (addNewElement) {
apiChangesSameHashCode.add(apiChange);
}
}
Collection<ApiChange> prunedCollection = new HashSet<ApiChange>();
for (Collection<ApiChange> changes : apiChangeMap.values()) {
prunedCollection.addAll(changes);
}
return prunedCollection;
}
/**
* Compares 2 APIs for source compatibility. Algorithm: First find packages
* that are in one but not in another. Then, look at at classes in the common
* packages. Look at public classes.
*
*/
private void computeApiDiff() throws NotFoundException {
Set<String> newApiPackageNames = newApi.getApiPackageNames();
missingPackageNames = oldApi.getApiPackageNames();
Set<String> intersection = removeIntersection(newApiPackageNames, missingPackageNames);
// Inspect each of the classes in each of the packages in the intersection
for (String packageName : intersection) {
ApiPackageDiffGenerator tempPackageDiffGenerator =
new ApiPackageDiffGenerator(packageName, this);
intersectingPackages.put(packageName, tempPackageDiffGenerator);
tempPackageDiffGenerator.computeApiDiff();
}
}
/**
* Returns how ApiClass for first element is "related" to the ApiClass for
* secondElement.
*/
private Relation getRelationOfApiClassOfFirstArgToThatOfSecond(ApiElement firstApiElement,
ApiElement secondApiElement) {
JClassType firstClassType = null;
JClassType secondClassType = null;
if (firstApiElement instanceof ApiField) {
firstClassType = ((ApiField) firstApiElement).getApiClass().getClassObject();
secondClassType = ((ApiField) secondApiElement).getApiClass().getClassObject();
}
if (firstApiElement instanceof ApiAbstractMethod) {
firstClassType = ((ApiAbstractMethod) firstApiElement).getApiClass().getClassObject();
secondClassType = ((ApiAbstractMethod) secondApiElement).getApiClass().getClassObject();
}
if (firstClassType != null && secondClassType != null) {
if (secondClassType.isAssignableTo(firstClassType)) {
return Relation.SUPERCLASS;
}
if (firstClassType.isAssignableTo(secondClassType)) {
return Relation.SUBCLASS;
}
return Relation.NONE;
}
throw new RuntimeException("Inconsistent types for ApiElements: newApiElement "
+ firstApiElement + ", oldApiElement : " + secondApiElement);
}
}