blob: 3c25bd257951766e5ed2aef27d238091de4c3222 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2011 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.rename;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IRegion;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringScopeFactory;
import org.eclipse.jdt.internal.corext.refactoring.base.ReferencesInBinaryContext;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.SearchUtils;
public class RippleMethodFinder2 {
private final IMethod fMethod;
private List<IMethod> fDeclarations;
private ITypeHierarchy fHierarchy;
private Map<IType, IMethod> fTypeToMethod;
private Set<IType> fRootTypes;
private MultiMap<IType, IType> fRootReps;
private Map<IType, ITypeHierarchy> fRootHierarchies;
private UnionFind fUnionFind;
private final boolean fExcludeBinaries;
private final ReferencesInBinaryContext fBinaryRefs;
private Map<IMethod, SearchMatch> fDeclarationToMatch;
private static class MultiMap<K, V> {
HashMap<K, Collection<V>> fImplementation= new HashMap<>();
public void put(K key, V value) {
Collection<V> collection= fImplementation.get(key);
if (collection == null) {
collection= new HashSet<>();
fImplementation.put(key, collection);
}
collection.add(value);
}
public Collection<V> get(K key) {
return fImplementation.get(key);
}
}
private static class UnionFind {
HashMap<IType, IType> fElementToRepresentative= new HashMap<>();
public void init(IType type) {
fElementToRepresentative.put(type, type);
}
//path compression:
public IType find(IType element) {
IType root= element;
IType rep= fElementToRepresentative.get(root);
while (rep != null && ! rep.equals(root)) {
root= rep;
rep= fElementToRepresentative.get(root);
}
if (rep == null)
return null;
rep= fElementToRepresentative.get(element);
while (! rep.equals(root)) {
IType temp= element;
element= rep;
fElementToRepresentative.put(temp, root);
rep= fElementToRepresentative.get(element);
}
return root;
}
// //straightforward:
// public IType find(IType element) {
// IType current= element;
// IType rep= (IType) fElementToRepresentative.get(current);
// while (rep != null && ! rep.equals(current)) {
// current= rep;
// rep= (IType) fElementToRepresentative.get(current);
// }
// if (rep == null)
// return null;
// else
// return current;
// }
public void union(IType rep1, IType rep2) {
fElementToRepresentative.put(rep1, rep2);
}
}
private RippleMethodFinder2(IMethod method, boolean excludeBinaries){
fMethod= method;
fExcludeBinaries= excludeBinaries;
fBinaryRefs= null;
}
private RippleMethodFinder2(IMethod method, ReferencesInBinaryContext binaryRefs) {
fMethod= method;
fExcludeBinaries= true;
fDeclarationToMatch= new HashMap<>();
fBinaryRefs= binaryRefs;
}
public static IMethod[] getRelatedMethods(IMethod method, boolean excludeBinaries, IProgressMonitor pm, WorkingCopyOwner owner) throws CoreException {
try{
if (! MethodChecks.isVirtual(method))
return new IMethod[]{ method };
return new RippleMethodFinder2(method, excludeBinaries).getAllRippleMethods(pm, owner);
} finally{
pm.done();
}
}
public static IMethod[] getRelatedMethods(IMethod method, IProgressMonitor pm, WorkingCopyOwner owner) throws CoreException {
return getRelatedMethods(method, true, pm, owner);
}
public static IMethod[] getRelatedMethods(IMethod method, ReferencesInBinaryContext binaryRefs, IProgressMonitor pm, WorkingCopyOwner owner) throws CoreException {
try {
if (! MethodChecks.isVirtual(method))
return new IMethod[]{ method };
return new RippleMethodFinder2(method, binaryRefs).getAllRippleMethods(pm, owner);
} finally{
pm.done();
}
}
private IMethod[] getAllRippleMethods(IProgressMonitor pm, WorkingCopyOwner owner) throws CoreException {
IMethod[] rippleMethods= findAllRippleMethods(pm, owner);
if (fDeclarationToMatch == null)
return rippleMethods;
List<IMethod> filteredMethods= new ArrayList<>(rippleMethods.length / 2);
for (IMethod currentMethod : rippleMethods) {
Object match= fDeclarationToMatch.get(currentMethod);
if (match != null) {
fBinaryRefs.add((SearchMatch) match);
} else {
filteredMethods.add(currentMethod);
}
}
fDeclarationToMatch= null;
return toArray(filteredMethods);
}
private IMethod[] findAllRippleMethods(IProgressMonitor pm, WorkingCopyOwner owner) throws CoreException {
pm.beginTask("", 4); //$NON-NLS-1$
findAllDeclarations(new SubProgressMonitor(pm, 1), owner);
//TODO: report assertion as error status and fall back to only return fMethod
//check for bug 81058:
if (! fDeclarations.contains(fMethod))
Assert.isTrue(false, "Search for method declaration did not find original element: " + fMethod.toString()); //$NON-NLS-1$
createHierarchyOfDeclarations(new SubProgressMonitor(pm, 1), owner);
createTypeToMethod();
createUnionFind();
checkCanceled(pm);
fHierarchy= null;
fRootTypes= null;
Map<IType, List<IType>> partitioning= new HashMap<>();
for (Iterator<IType> iter= fTypeToMethod.keySet().iterator(); iter.hasNext();) {
IType type= iter.next();
IType rep= fUnionFind.find(type);
List<IType> types= partitioning.get(rep);
if (types == null)
types= new ArrayList<>();
types.add(type);
partitioning.put(rep, types);
}
Assert.isTrue(partitioning.size() > 0);
if (partitioning.size() == 1)
return fDeclarations.toArray(new IMethod[fDeclarations.size()]);
//Multiple partitions; must look out for nasty marriage cases
//(types inheriting method from two ancestors, but without redeclaring it).
IType methodTypeRep= fUnionFind.find(fMethod.getDeclaringType());
List<IType> relatedTypes= partitioning.get(methodTypeRep);
boolean hasRelatedInterfaces= false;
List<IMethod> relatedMethods= new ArrayList<>();
for (Iterator<IType> iter= relatedTypes.iterator(); iter.hasNext();) {
IType relatedType= iter.next();
relatedMethods.add(fTypeToMethod.get(relatedType));
if (relatedType.isInterface())
hasRelatedInterfaces= true;
}
int numberOfSearchMatches= fDeclarations.size();
//Definition: An alien type is a type that is not a related type. The set of
// alien types diminishes as new types become related (a.k.a marry a relatedType).
Set<IMethod> alienDeclarations= new LinkedHashSet<>(fDeclarations);
fDeclarations= null;
alienDeclarations.removeAll(relatedMethods);
Set<IType> alienTypes= new LinkedHashSet<>();
boolean hasAlienInterfaces= false;
for (Iterator<IMethod> iter= alienDeclarations.iterator(); iter.hasNext();) {
IMethod alienDeclaration= iter.next();
IType alienType= alienDeclaration.getDeclaringType();
alienTypes.add(alienType);
if (alienType.isInterface())
hasAlienInterfaces= true;
}
if (alienTypes.size() == 0) //no nasty marriage scenarios without types to marry with...
return toArray(relatedMethods);
if (! hasRelatedInterfaces && ! hasAlienInterfaces) //no nasty marriage scenarios without interfaces...
return toArray(relatedMethods);
/*
* Go down the hierarchy of the type under rename, and build hierarchies of its sub-types.
* Check if any sub-type implements another interface with the same method name.
* If not, we cannot have married alien types, so we can skip building type hierarchies for the alien types.
*/
checkCanceled(pm);
IType methodType= fMethod.getDeclaringType();
ITypeHierarchy methodHierarchy= hierarchy(pm, owner, fUnionFind.find(methodType));
IType[] methodTypeSubtypes= methodHierarchy.getAllSubtypes(methodType);
// don't spend time on this check, unless we have a small hierarchy for the type under rename and a lot of search matches
if (methodTypeSubtypes.length <= numberOfSearchMatches / 10) {
boolean couldHaveMarriedAlienTypes= couldHaveMarriedAlienTypes(pm, owner, methodHierarchy, methodTypeSubtypes);
if (!couldHaveMarriedAlienTypes) {
return toArray(relatedMethods);
}
}
//find all subtypes of related types:
HashSet<IType> relatedSubTypes= new HashSet<>();
List<IType> relatedTypesToProcess= new ArrayList<>(relatedTypes);
while (relatedTypesToProcess.size() > 0) {
//TODO: would only need subtype hierarchies of all top-of-ripple relatedTypesToProcess
for (Iterator<IType> iter= relatedTypesToProcess.iterator(); iter.hasNext();) {
checkCanceled(pm);
IType relatedType= iter.next();
ITypeHierarchy hierarchy= hierarchy(pm, owner, relatedType);
IType[] allSubTypes= hierarchy.getAllSubtypes(relatedType);
relatedSubTypes.addAll(Arrays.asList(allSubTypes));
}
relatedTypesToProcess.clear(); //processed; make sure loop terminates
HashSet<IType> marriedAlienTypeReps= new HashSet<>();
for (Iterator<IType> iter= alienTypes.iterator(); iter.hasNext();) {
checkCanceled(pm);
IType alienType= iter.next();
IMethod alienMethod= fTypeToMethod.get(alienType);
ITypeHierarchy hierarchy= hierarchy(pm, owner, alienType);
for (IType subtype : hierarchy.getAllSubtypes(alienType)) {
if (relatedSubTypes.contains(subtype)) {
if (JavaModelUtil.isVisibleInHierarchy(alienMethod, subtype.getPackageFragment())) {
marriedAlienTypeReps.add(fUnionFind.find(alienType));
} else {
// not overridden
}
}
}
}
if (marriedAlienTypeReps.size() == 0)
return toArray(relatedMethods);
for (Iterator<IType> iter= marriedAlienTypeReps.iterator(); iter.hasNext();) {
IType marriedAlienTypeRep= iter.next();
List<IType> marriedAlienTypes= partitioning.get(marriedAlienTypeRep);
for (Iterator<IType> iterator= marriedAlienTypes.iterator(); iterator.hasNext();) {
IType marriedAlienInterfaceType= iterator.next();
relatedMethods.add(fTypeToMethod.get(marriedAlienInterfaceType));
}
alienTypes.removeAll(marriedAlienTypes); //not alien any more
relatedTypesToProcess.addAll(marriedAlienTypes); //process freshly married types again
}
}
fRootReps= null;
fRootHierarchies= null;
fTypeToMethod= null;
fUnionFind= null;
return toArray(relatedMethods);
}
/**
* For the method under rename, checks if any sub-type inherits a method with the same name from a different super type.
*
* @param pm progress monitor to
* @param owner owner of the compilation unit under rename
* @param methodHierarchy the type hierarchy of the method under rename
* @param methodTypeSubtypes the sub-types of the type under rename
* @return {@code false} if there can be no married alien types to the method under rename, {@code true} otherwise.
* @throws JavaModelException if creating a type hierarchy fails
*/
private boolean couldHaveMarriedAlienTypes(IProgressMonitor pm, WorkingCopyOwner owner, ITypeHierarchy methodHierarchy, IType[] methodTypeSubtypes) throws JavaModelException {
Set<IType> allTypesInMethodHierarchy= new HashSet<>();
allTypesInMethodHierarchy.addAll(Arrays.asList(methodHierarchy.getAllClasses()));
allTypesInMethodHierarchy.addAll(Arrays.asList(methodHierarchy.getAllInterfaces()));
for (IType methodTypeSubtype : methodTypeSubtypes) {
checkCanceled(pm);
ITypeHierarchy subtypeHierarchy= methodTypeSubtype.newTypeHierarchy(owner, pm);
IType[] subtypeSuperTypes= subtypeHierarchy.getAllSupertypes(methodTypeSubtype);
for (IType subtypeSuperType : subtypeSuperTypes) {
checkCanceled(pm);
if (!allTypesInMethodHierarchy.contains(subtypeSuperType)) {
if (definesSimilarMethod(subtypeSuperType, fMethod)) {
return true;
}
}
}
}
return false;
}
/**
* @return {@code true} if the specified type defines a method which shares method name and parameter types with the specified method.
*
* @param type the type to check in
* @param method the method to check for
* @throws JavaModelException if listing the methods of the type fails
*/
private static boolean definesSimilarMethod(IType type, IMethod method) throws JavaModelException {
/*
* We don't use type.getMethod(method.getElementName(), method.getParameterTypes()),
* since types that come from binaries can list different parameter names. So we could miss a matching definition.
*/
String methodName= method.getElementName();
IMethod[] typeMethods= type.getMethods();
return Arrays.asList(typeMethods).stream().anyMatch(typeMethod -> methodName.equals(typeMethod.getElementName()));
}
private static IMethod[] toArray(List<IMethod> methods) {
return methods.toArray(new IMethod[methods.size()]);
}
private static void checkCanceled(IProgressMonitor pm) {
if (pm.isCanceled()) {
throw new OperationCanceledException();
}
}
private ITypeHierarchy hierarchy(IProgressMonitor pm, WorkingCopyOwner owner, IType type)
throws JavaModelException {
ITypeHierarchy hierarchy= getCachedHierarchy(type, owner, new SubProgressMonitor(pm, 1));
if (hierarchy == null)
hierarchy= type.newTypeHierarchy(owner, new SubProgressMonitor(pm, 1));
return hierarchy;
}
private ITypeHierarchy getCachedHierarchy(IType type, WorkingCopyOwner owner, IProgressMonitor monitor) throws JavaModelException {
IType rep= fUnionFind.find(type);
if (rep != null) {
Collection<IType> collection= fRootReps.get(rep);
for (Iterator<IType> iter= collection.iterator(); iter.hasNext();) {
IType root= iter.next();
ITypeHierarchy hierarchy= fRootHierarchies.get(root);
if (hierarchy == null) {
hierarchy= root.newTypeHierarchy(owner, new SubProgressMonitor(monitor, 1));
fRootHierarchies.put(root, hierarchy);
}
if (hierarchy.contains(type))
return hierarchy;
}
}
return null;
}
private void findAllDeclarations(IProgressMonitor monitor, WorkingCopyOwner owner) throws CoreException {
fDeclarations= new ArrayList<>();
class MethodRequestor extends SearchRequestor {
@Override
public void acceptSearchMatch(SearchMatch match) throws CoreException {
IMethod method= (IMethod) match.getElement();
boolean isVisible= JavaModelUtil.isVisibleInHierarchy(method, fMethod.getDeclaringType().getPackageFragment());
if (isVisible) {
boolean isBinary= method.isBinary();
if (fBinaryRefs != null || ! (fExcludeBinaries && isBinary)) {
fDeclarations.add(method);
}
if (isBinary && fBinaryRefs != null) {
fDeclarationToMatch.put(method, match);
}
}
}
}
int limitTo= IJavaSearchConstants.DECLARATIONS | IJavaSearchConstants.IGNORE_DECLARING_TYPE | IJavaSearchConstants.IGNORE_RETURN_TYPE;
int matchRule= SearchPattern.R_ERASURE_MATCH | SearchPattern.R_CASE_SENSITIVE;
SearchPattern pattern= SearchPattern.createPattern(fMethod, limitTo, matchRule);
SearchParticipant[] participants= SearchUtils.getDefaultSearchParticipants();
IJavaSearchScope scope= RefactoringScopeFactory.createRelatedProjectsScope(fMethod.getJavaProject(), IJavaSearchScope.SOURCES | IJavaSearchScope.APPLICATION_LIBRARIES | IJavaSearchScope.SYSTEM_LIBRARIES);
MethodRequestor requestor= new MethodRequestor();
SearchEngine searchEngine= owner != null ? new SearchEngine(owner) : new SearchEngine();
searchEngine.search(pattern, participants, scope, requestor, monitor);
}
private void createHierarchyOfDeclarations(IProgressMonitor pm, WorkingCopyOwner owner) throws JavaModelException {
Stream<IType> types= fDeclarations.stream().map(method -> method.getDeclaringType());
fHierarchy= createHierarchyOfTypes(pm, owner, types);
}
private static ITypeHierarchy createHierarchyOfTypes(IProgressMonitor pm, WorkingCopyOwner owner, Stream<IType> types) throws JavaModelException {
IRegion region= JavaCore.newRegion();
for (Iterator<IType> iter= types.iterator(); iter.hasNext(); ) {
IType type= iter.next();
region.add(type);
}
return JavaCore.newTypeHierarchy(region, owner, pm);
}
private void createTypeToMethod() {
fTypeToMethod= new HashMap<>();
for (Iterator<IMethod> iter= fDeclarations.iterator(); iter.hasNext();) {
IMethod declaration= iter.next();
fTypeToMethod.put(declaration.getDeclaringType(), declaration);
}
}
private void createUnionFind() throws JavaModelException {
fRootTypes= new HashSet<>(fTypeToMethod.keySet());
fUnionFind= new UnionFind();
for (Iterator<IType> iter= fTypeToMethod.keySet().iterator(); iter.hasNext();) {
IType type= iter.next();
fUnionFind.init(type);
}
for (Iterator<IType> iter= fTypeToMethod.keySet().iterator(); iter.hasNext();) {
IType type= iter.next();
uniteWithSupertypes(type, type);
}
fRootReps= new MultiMap<>();
for (Iterator<IType> iter= fRootTypes.iterator(); iter.hasNext();) {
IType type= iter.next();
IType rep= fUnionFind.find(type);
if (rep != null)
fRootReps.put(rep, type);
}
fRootHierarchies= new HashMap<>();
}
private void uniteWithSupertypes(IType anchor, IType type) throws JavaModelException {
for (IType supertype : fHierarchy.getSupertypes(type)) {
IType superRep= fUnionFind.find(supertype);
if (superRep == null) {
//Type doesn't declare method, but maybe supertypes?
uniteWithSupertypes(anchor, supertype);
} else {
//check whether method in supertype is really overridden:
IMember superMethod= fTypeToMethod.get(supertype);
if (JavaModelUtil.isVisibleInHierarchy(superMethod, anchor.getPackageFragment())) {
IType rep= fUnionFind.find(anchor);
fUnionFind.union(rep, superRep);
// current type is no root anymore
fRootTypes.remove(anchor);
uniteWithSupertypes(supertype, supertype);
} else {
//Not overridden -> overriding chain ends here.
}
}
}
}
}