blob: b8f069d5f1420a9a8321f92a6cee75794f53f940 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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.intellij.refactoring.safeDelete;
import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter;
import com.intellij.lang.LanguageRefactoringSupport;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lang.refactoring.RefactoringSupportProvider;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.BaseRefactoringProcessor;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.listeners.RefactoringEventData;
import com.intellij.refactoring.listeners.RefactoringEventListener;
import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteCustomUsageInfo;
import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteReferenceSimpleDeleteUsageInfo;
import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteReferenceUsageInfo;
import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteUsageInfo;
import com.intellij.refactoring.util.NonCodeSearchDescriptionLocation;
import com.intellij.refactoring.util.RefactoringUIUtil;
import com.intellij.refactoring.util.TextOccurrencesUtil;
import com.intellij.usageView.UsageInfo;
import com.intellij.usageView.UsageViewDescriptor;
import com.intellij.usageView.UsageViewUtil;
import com.intellij.usages.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.Processor;
import com.intellij.util.containers.HashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author dsl
*/
public class SafeDeleteProcessor extends BaseRefactoringProcessor {
private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.safeDelete.SafeDeleteProcessor");
private final PsiElement[] myElements;
private boolean mySearchInCommentsAndStrings;
private boolean mySearchNonJava;
private boolean myPreviewNonCodeUsages = true;
private SafeDeleteProcessor(Project project, @Nullable Runnable prepareSuccessfulCallback,
PsiElement[] elementsToDelete, boolean isSearchInComments, boolean isSearchNonJava) {
super(project, prepareSuccessfulCallback);
myElements = elementsToDelete;
mySearchInCommentsAndStrings = isSearchInComments;
mySearchNonJava = isSearchNonJava;
}
@Override
@NotNull
protected UsageViewDescriptor createUsageViewDescriptor(UsageInfo[] usages) {
return new SafeDeleteUsageViewDescriptor(myElements);
}
private static boolean isInside(PsiElement place, PsiElement[] ancestors) {
return isInside(place, Arrays.asList(ancestors));
}
private static boolean isInside(PsiElement place, Collection<? extends PsiElement> ancestors) {
for (PsiElement element : ancestors) {
if (isInside(place, element)) return true;
}
return false;
}
public static boolean isInside (PsiElement place, PsiElement ancestor) {
if (ancestor instanceof PsiDirectoryContainer) {
final PsiDirectory[] directories = ((PsiDirectoryContainer)ancestor).getDirectories(place.getResolveScope());
for (PsiDirectory directory : directories) {
if (isInside(place, directory)) return true;
}
}
if (ancestor instanceof PsiFile) {
for (PsiFile file : ((PsiFile)ancestor).getViewProvider().getAllFiles()) {
if (PsiTreeUtil.isAncestor(file, place, false)) return true;
}
}
boolean isAncestor = PsiTreeUtil.isAncestor(ancestor, place, false);
if (!isAncestor && ancestor instanceof PsiNameIdentifierOwner) {
final PsiElement nameIdentifier = ((PsiNameIdentifierOwner)ancestor).getNameIdentifier();
if (nameIdentifier != null && !PsiTreeUtil.isAncestor(ancestor, nameIdentifier, true)) {
isAncestor = PsiTreeUtil.isAncestor(nameIdentifier.getParent(), place, false);
}
}
if (!isAncestor) {
final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(place.getProject());
PsiLanguageInjectionHost host = injectedLanguageManager.getInjectionHost(place);
while (host != null) {
if (PsiTreeUtil.isAncestor(ancestor, host, false)) {
isAncestor = true;
break;
}
host = injectedLanguageManager.getInjectionHost(host);
}
}
return isAncestor;
}
@Override
@NotNull
protected UsageInfo[] findUsages() {
List<UsageInfo> usages = Collections.synchronizedList(new ArrayList<UsageInfo>());
for (PsiElement element : myElements) {
boolean handled = false;
for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
if (delegate.handlesElement(element)) {
final NonCodeUsageSearchInfo filter = delegate.findUsages(element, myElements, usages);
if (filter != null) {
for(PsiElement nonCodeUsageElement: filter.getElementsToSearch()) {
addNonCodeUsages(nonCodeUsageElement, usages, filter.getInsideDeletedCondition());
}
}
handled = true;
break;
}
}
if (!handled && element instanceof PsiNamedElement) {
findGenericElementUsages(element, usages, myElements);
addNonCodeUsages(element, usages, getDefaultInsideDeletedCondition(myElements));
}
}
final UsageInfo[] result = usages.toArray(new UsageInfo[usages.size()]);
return UsageViewUtil.removeDuplicatedUsages(result);
}
public static Condition<PsiElement> getDefaultInsideDeletedCondition(final PsiElement[] elements) {
return new Condition<PsiElement>() {
@Override
public boolean value(final PsiElement usage) {
return !(usage instanceof PsiFile) && isInside(usage, elements);
}
};
}
public static void findGenericElementUsages(final PsiElement element, final List<UsageInfo> usages, final PsiElement[] allElementsToDelete) {
ReferencesSearch.search(element).forEach(new Processor<PsiReference>() {
@Override
public boolean process(final PsiReference reference) {
final PsiElement refElement = reference.getElement();
if (!isInside(refElement, allElementsToDelete)) {
usages.add(new SafeDeleteReferenceSimpleDeleteUsageInfo(refElement, element, false));
}
return true;
}
});
}
@Override
protected boolean preprocessUsages(Ref<UsageInfo[]> refUsages) {
UsageInfo[] usages = refUsages.get();
ArrayList<String> conflicts = new ArrayList<String>();
for (PsiElement element : myElements) {
for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
if (delegate.handlesElement(element)) {
Collection<String> foundConflicts = delegate.findConflicts(element, myElements);
if (foundConflicts != null) {
conflicts.addAll(foundConflicts);
}
break;
}
}
}
final HashMap<PsiElement,UsageHolder> elementsToUsageHolders = sortUsages(usages);
final Collection<UsageHolder> usageHolders = elementsToUsageHolders.values();
for (UsageHolder usageHolder : usageHolders) {
if (usageHolder.hasUnsafeUsagesInCode()) {
conflicts.add(usageHolder.getDescription());
}
}
if (!conflicts.isEmpty()) {
final RefactoringEventData conflictData = new RefactoringEventData();
conflictData.putUserData(RefactoringEventData.CONFLICTS_KEY, conflicts);
myProject.getMessageBus().syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC).conflictsDetected("refactoring.safeDelete", conflictData);
if (ApplicationManager.getApplication().isUnitTestMode()) {
if (!ConflictsInTestsException.isTestIgnore()) throw new ConflictsInTestsException(conflicts);
}
else {
UnsafeUsagesDialog dialog = new UnsafeUsagesDialog(ArrayUtil.toStringArray(conflicts), myProject);
dialog.show();
if (!dialog.isOK()) {
final int exitCode = dialog.getExitCode();
prepareSuccessful(); // dialog is always dismissed
if (exitCode == UnsafeUsagesDialog.VIEW_USAGES_EXIT_CODE) {
showUsages(usages);
}
return false;
}
else {
myPreviewNonCodeUsages = false;
}
}
}
UsageInfo[] preprocessedUsages = usages;
for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
preprocessedUsages = delegate.preprocessUsages(myProject, preprocessedUsages);
if (preprocessedUsages == null) return false;
}
final UsageInfo[] filteredUsages = UsageViewUtil.removeDuplicatedUsages(preprocessedUsages);
prepareSuccessful(); // dialog is always dismissed
if(filteredUsages == null) {
return false;
}
refUsages.set(filteredUsages);
return true;
}
private void showUsages(final UsageInfo[] usages) {
UsageViewPresentation presentation = new UsageViewPresentation();
presentation.setTabText(RefactoringBundle.message("safe.delete.title"));
presentation.setTargetsNodeText(RefactoringBundle.message("attempting.to.delete.targets.node.text"));
presentation.setShowReadOnlyStatusAsRed(true);
presentation.setShowCancelButton(true);
presentation.setCodeUsagesString(RefactoringBundle.message("references.found.in.code"));
presentation.setUsagesInGeneratedCodeString(RefactoringBundle.message("references.found.in.generated.code"));
presentation.setNonCodeUsagesString(RefactoringBundle.message("occurrences.found.in.comments.strings.and.non.java.files"));
presentation.setUsagesString(RefactoringBundle.message("usageView.usagesText"));
UsageViewManager manager = UsageViewManager.getInstance(myProject);
final UsageView usageView = showUsages(usages, presentation, manager);
usageView.addPerformOperationAction(new RerunSafeDelete(myProject, myElements, usageView),
RefactoringBundle.message("retry.command"), null, RefactoringBundle.message("rerun.safe.delete"));
usageView.addPerformOperationAction(new Runnable() {
@Override
public void run() {
UsageInfo[] preprocessedUsages = usages;
for (SafeDeleteProcessorDelegate delegate : Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
preprocessedUsages = delegate.preprocessUsages(myProject, preprocessedUsages);
if (preprocessedUsages == null) return;
}
final UsageInfo[] filteredUsages = UsageViewUtil.removeDuplicatedUsages(preprocessedUsages);
execute(filteredUsages);
}
}, "Delete Anyway", RefactoringBundle.message("usageView.need.reRun"), RefactoringBundle.message("usageView.doAction"));
}
private UsageView showUsages(UsageInfo[] usages, UsageViewPresentation presentation, UsageViewManager manager) {
for (SafeDeleteProcessorDelegate delegate : Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
if (delegate instanceof SafeDeleteProcessorDelegateBase) {
final UsageView view = ((SafeDeleteProcessorDelegateBase)delegate).showUsages(usages, presentation, manager, myElements);
if (view != null) return view;
}
}
UsageTarget[] targets = new UsageTarget[myElements.length];
for (int i = 0; i < targets.length; i++) {
targets[i] = new PsiElement2UsageTargetAdapter(myElements[i]);
}
return manager.showUsages(targets,
UsageInfoToUsageConverter.convert(myElements, usages),
presentation
);
}
public PsiElement[] getElements() {
return myElements;
}
private static class RerunSafeDelete implements Runnable {
final SmartPsiElementPointer[] myPointers;
private final Project myProject;
private final UsageView myUsageView;
RerunSafeDelete(Project project, PsiElement[] elements, UsageView usageView) {
myProject = project;
myUsageView = usageView;
myPointers = new SmartPsiElementPointer[elements.length];
for (int i = 0; i < elements.length; i++) {
PsiElement element = elements[i];
myPointers[i] = SmartPointerManager.getInstance(myProject).createSmartPsiElementPointer(element);
}
}
@Override
public void run() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
myUsageView.close();
ArrayList<PsiElement> elements = new ArrayList<PsiElement>();
for (SmartPsiElementPointer pointer : myPointers) {
final PsiElement element = pointer.getElement();
if (element != null) {
elements.add(element);
}
}
if(!elements.isEmpty()) {
SafeDeleteHandler.invoke(myProject, PsiUtilCore.toPsiElementArray(elements), true);
}
}
});
}
}
/**
* @param usages
* @return Map from elements to UsageHolders
*/
private static HashMap<PsiElement,UsageHolder> sortUsages(UsageInfo[] usages) {
HashMap<PsiElement,UsageHolder> result = new HashMap<PsiElement, UsageHolder>();
for (final UsageInfo usage : usages) {
if (usage instanceof SafeDeleteUsageInfo) {
final PsiElement referencedElement = ((SafeDeleteUsageInfo)usage).getReferencedElement();
if (!result.containsKey(referencedElement)) {
result.put(referencedElement, new UsageHolder(referencedElement, usages));
}
}
}
return result;
}
@Override
protected void refreshElements(PsiElement[] elements) {
LOG.assertTrue(elements.length == myElements.length);
System.arraycopy(elements, 0, myElements, 0, elements.length);
}
@Override
protected boolean isPreviewUsages(UsageInfo[] usages) {
if(myPreviewNonCodeUsages && UsageViewUtil.reportNonRegularUsages(usages, myProject)) {
return true;
}
return super.isPreviewUsages(filterToBeDeleted(usages));
}
private static UsageInfo[] filterToBeDeleted(UsageInfo[] infos) {
ArrayList<UsageInfo> list = new ArrayList<UsageInfo>();
for (UsageInfo info : infos) {
if (!(info instanceof SafeDeleteReferenceUsageInfo) || ((SafeDeleteReferenceUsageInfo) info).isSafeDelete()) {
list.add(info);
}
}
return list.toArray(new UsageInfo[list.size()]);
}
@Nullable
@Override
protected RefactoringEventData getBeforeData() {
final RefactoringEventData beforeData = new RefactoringEventData();
beforeData.addElements(myElements);
return beforeData;
}
@Nullable
@Override
protected String getRefactoringId() {
return "refactoring.safeDelete";
}
@Override
protected void performRefactoring(UsageInfo[] usages) {
try {
for (UsageInfo usage : usages) {
if (usage instanceof SafeDeleteCustomUsageInfo) {
((SafeDeleteCustomUsageInfo) usage).performRefactoring();
}
}
for (PsiElement element : myElements) {
for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
if (delegate.handlesElement(element)) {
delegate.prepareForDeletion(element);
}
}
element.delete();
}
} catch (IncorrectOperationException e) {
RefactoringUIUtil.processIncorrectOperation(myProject, e);
}
}
private String calcCommandName() {
return RefactoringBundle.message("safe.delete.command", RefactoringUIUtil.calculatePsiElementDescriptionList(myElements));
}
private String myCachedCommandName = null;
@Override
protected String getCommandName() {
if (myCachedCommandName == null) {
myCachedCommandName = calcCommandName();
}
return myCachedCommandName;
}
private void addNonCodeUsages(final PsiElement element, List<UsageInfo> usages, @Nullable final Condition<PsiElement> insideElements) {
TextOccurrencesUtil.UsageInfoFactory nonCodeUsageFactory = new TextOccurrencesUtil.UsageInfoFactory() {
@Override
public UsageInfo createUsageInfo(@NotNull PsiElement usage, int startOffset, int endOffset) {
if (insideElements != null && insideElements.value(usage)) {
return null;
}
return new SafeDeleteReferenceSimpleDeleteUsageInfo(usage, element, startOffset, endOffset, true, false);
}
};
if (mySearchInCommentsAndStrings) {
String stringToSearch = ElementDescriptionUtil.getElementDescription(element, NonCodeSearchDescriptionLocation.STRINGS_AND_COMMENTS);
TextOccurrencesUtil.addUsagesInStringsAndComments(element, stringToSearch, usages, nonCodeUsageFactory);
}
if (mySearchNonJava) {
String stringToSearch = ElementDescriptionUtil.getElementDescription(element, NonCodeSearchDescriptionLocation.NON_JAVA);
TextOccurrencesUtil.addTextOccurences(element, stringToSearch, GlobalSearchScope.projectScope(myProject), usages, nonCodeUsageFactory);
}
}
@Override
protected boolean isToBeChanged(UsageInfo usageInfo) {
if (usageInfo instanceof SafeDeleteReferenceUsageInfo) {
return ((SafeDeleteReferenceUsageInfo)usageInfo).isSafeDelete() && super.isToBeChanged(usageInfo);
}
return super.isToBeChanged(usageInfo);
}
public static boolean validElement(@NotNull PsiElement element) {
if (element instanceof PsiFile) return true;
if (!element.isPhysical()) return false;
final RefactoringSupportProvider provider = LanguageRefactoringSupport.INSTANCE.forLanguage(element.getLanguage());
return provider.isSafeDeleteAvailable(element);
}
public static SafeDeleteProcessor createInstance(Project project, @Nullable Runnable prepareSuccessfulCallback,
PsiElement[] elementsToDelete, boolean isSearchInComments, boolean isSearchNonJava) {
return new SafeDeleteProcessor(project, prepareSuccessfulCallback, elementsToDelete, isSearchInComments, isSearchNonJava);
}
public static SafeDeleteProcessor createInstance(Project project, @Nullable Runnable prepareSuccessfulCallBack,
PsiElement[] elementsToDelete, boolean isSearchInComments, boolean isSearchNonJava,
boolean askForAccessors) {
ArrayList<PsiElement> elements = new ArrayList<PsiElement>(Arrays.asList(elementsToDelete));
HashSet<PsiElement> elementsToDeleteSet = new HashSet<PsiElement>(Arrays.asList(elementsToDelete));
for (PsiElement psiElement : elementsToDelete) {
for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
if (delegate.handlesElement(psiElement)) {
Collection<PsiElement> addedElements = delegate.getAdditionalElementsToDelete(psiElement, elementsToDeleteSet, askForAccessors);
if (addedElements != null) {
elements.addAll(addedElements);
}
break;
}
}
}
return new SafeDeleteProcessor(project, prepareSuccessfulCallBack,
PsiUtilCore.toPsiElementArray(elements),
isSearchInComments, isSearchNonJava);
}
public boolean isSearchInCommentsAndStrings() {
return mySearchInCommentsAndStrings;
}
public void setSearchInCommentsAndStrings(boolean searchInCommentsAndStrings) {
mySearchInCommentsAndStrings = searchInCommentsAndStrings;
}
public boolean isSearchNonJava() {
return mySearchNonJava;
}
public void setSearchNonJava(boolean searchNonJava) {
mySearchNonJava = searchNonJava;
}
@Override
protected boolean skipNonCodeUsages() {
return true;
}
}