IDEA-143309 Throwable on safe delete of a project file
[idea/community.git] / platform / lang-impl / src / com / intellij / refactoring / safeDelete / SafeDeleteProcessor.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.refactoring.safeDelete;
18
19 import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter;
20 import com.intellij.lang.LanguageRefactoringSupport;
21 import com.intellij.lang.injection.InjectedLanguageManager;
22 import com.intellij.lang.refactoring.RefactoringSupportProvider;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.extensions.Extensions;
26 import com.intellij.openapi.project.DumbModePermission;
27 import com.intellij.openapi.project.DumbService;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.project.ProjectManager;
30 import com.intellij.openapi.util.Condition;
31 import com.intellij.openapi.util.Ref;
32 import com.intellij.psi.*;
33 import com.intellij.psi.search.GlobalSearchScope;
34 import com.intellij.psi.search.searches.ReferencesSearch;
35 import com.intellij.psi.util.PsiTreeUtil;
36 import com.intellij.psi.util.PsiUtilCore;
37 import com.intellij.refactoring.BaseRefactoringProcessor;
38 import com.intellij.refactoring.RefactoringBundle;
39 import com.intellij.refactoring.listeners.RefactoringEventData;
40 import com.intellij.refactoring.listeners.RefactoringEventListener;
41 import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteCustomUsageInfo;
42 import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteReferenceSimpleDeleteUsageInfo;
43 import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteReferenceUsageInfo;
44 import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteUsageInfo;
45 import com.intellij.refactoring.util.NonCodeSearchDescriptionLocation;
46 import com.intellij.refactoring.util.RefactoringUIUtil;
47 import com.intellij.refactoring.util.TextOccurrencesUtil;
48 import com.intellij.usageView.UsageInfo;
49 import com.intellij.usageView.UsageInfoFactory;
50 import com.intellij.usageView.UsageViewDescriptor;
51 import com.intellij.usageView.UsageViewUtil;
52 import com.intellij.usages.*;
53 import com.intellij.util.ArrayUtil;
54 import com.intellij.util.IncorrectOperationException;
55 import com.intellij.util.Processor;
56 import com.intellij.util.containers.HashMap;
57 import org.jetbrains.annotations.NotNull;
58 import org.jetbrains.annotations.Nullable;
59
60 import java.util.*;
61
62 /**
63  * @author dsl
64  */
65 public class SafeDeleteProcessor extends BaseRefactoringProcessor {
66   private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.safeDelete.SafeDeleteProcessor");
67   private final PsiElement[] myElements;
68   private boolean mySearchInCommentsAndStrings;
69   private boolean mySearchNonJava;
70   private boolean myPreviewNonCodeUsages = true;
71
72   private SafeDeleteProcessor(Project project, @Nullable Runnable prepareSuccessfulCallback,
73                               PsiElement[] elementsToDelete, boolean isSearchInComments, boolean isSearchNonJava) {
74     super(project, prepareSuccessfulCallback);
75     myElements = elementsToDelete;
76     mySearchInCommentsAndStrings = isSearchInComments;
77     mySearchNonJava = isSearchNonJava;
78   }
79
80   @Override
81   @NotNull
82   protected UsageViewDescriptor createUsageViewDescriptor(@NotNull UsageInfo[] usages) {
83     return new SafeDeleteUsageViewDescriptor(myElements);
84   }
85
86   private static boolean isInside(PsiElement place, PsiElement[] ancestors) {
87     return isInside(place, Arrays.asList(ancestors));
88   }
89
90   private static boolean isInside(PsiElement place, Collection<? extends PsiElement> ancestors) {
91     for (PsiElement element : ancestors) {
92       if (isInside(place, element)) return true;
93     }
94     return false;
95   }
96
97   public static boolean isInside (PsiElement place, PsiElement ancestor) {
98     if (ancestor instanceof PsiDirectoryContainer) {
99       final PsiDirectory[] directories = ((PsiDirectoryContainer)ancestor).getDirectories(place.getResolveScope());
100       for (PsiDirectory directory : directories) {
101         if (isInside(place, directory)) return true;
102       }
103     }
104
105     if (ancestor instanceof PsiFile) {
106       for (PsiFile file : ((PsiFile)ancestor).getViewProvider().getAllFiles()) {
107         if (PsiTreeUtil.isAncestor(file, place, false)) return true;
108       }
109     }
110
111     boolean isAncestor = PsiTreeUtil.isAncestor(ancestor, place, false);
112     if (!isAncestor && ancestor instanceof PsiNameIdentifierOwner) {
113       final PsiElement nameIdentifier = ((PsiNameIdentifierOwner)ancestor).getNameIdentifier();
114       if (nameIdentifier != null && !PsiTreeUtil.isAncestor(ancestor, nameIdentifier, true)) {
115         isAncestor = PsiTreeUtil.isAncestor(nameIdentifier.getParent(), place, false);
116       }
117     }
118
119     if (!isAncestor) {
120       final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(place.getProject());
121       PsiLanguageInjectionHost host = injectedLanguageManager.getInjectionHost(place);
122       while (host != null) {
123         if (PsiTreeUtil.isAncestor(ancestor, host, false)) {
124           isAncestor = true;
125           break;
126         }
127         host = injectedLanguageManager.getInjectionHost(host);
128       }
129     }
130     return isAncestor;
131   }
132
133   @Override
134   @NotNull
135   protected UsageInfo[] findUsages() {
136     List<UsageInfo> usages = Collections.synchronizedList(new ArrayList<UsageInfo>());
137     for (PsiElement element : myElements) {
138       boolean handled = false;
139       for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
140         if (delegate.handlesElement(element)) {
141           final NonCodeUsageSearchInfo filter = delegate.findUsages(element, myElements, usages);
142           if (filter != null) {
143             for(PsiElement nonCodeUsageElement: filter.getElementsToSearch()) {
144               addNonCodeUsages(nonCodeUsageElement, usages, filter.getInsideDeletedCondition(), mySearchNonJava,
145                                mySearchInCommentsAndStrings);
146             }
147           }
148           handled = true;
149           break;
150         }
151       }
152       if (!handled && element instanceof PsiNamedElement) {
153         findGenericElementUsages(element, usages, myElements);
154         addNonCodeUsages(element, usages, getDefaultInsideDeletedCondition(myElements), mySearchNonJava, mySearchInCommentsAndStrings);
155       }
156     }
157     final UsageInfo[] result = usages.toArray(new UsageInfo[usages.size()]);
158     return UsageViewUtil.removeDuplicatedUsages(result);
159   }
160
161   public static Condition<PsiElement> getDefaultInsideDeletedCondition(final PsiElement[] elements) {
162     return new Condition<PsiElement>() {
163       @Override
164       public boolean value(final PsiElement usage) {
165         return !(usage instanceof PsiFile) && isInside(usage, elements);
166       }
167     };
168   }
169
170   public static void findGenericElementUsages(final PsiElement element, final List<UsageInfo> usages, final PsiElement[] allElementsToDelete) {
171     ReferencesSearch.search(element).forEach(new Processor<PsiReference>() {
172       @Override
173       public boolean process(final PsiReference reference) {
174         final PsiElement refElement = reference.getElement();
175         if (!isInside(refElement, allElementsToDelete)) {
176           usages.add(new SafeDeleteReferenceSimpleDeleteUsageInfo(refElement, element, false));
177         }
178         return true;
179       }
180     });
181   }
182
183   @Override
184   protected boolean preprocessUsages(@NotNull Ref<UsageInfo[]> refUsages) {
185     UsageInfo[] usages = refUsages.get();
186     ArrayList<String> conflicts = new ArrayList<String>();
187
188     for (PsiElement element : myElements) {
189       for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
190         if (delegate.handlesElement(element)) {
191           Collection<String> foundConflicts = delegate.findConflicts(element, myElements);
192           if (foundConflicts != null) {
193             conflicts.addAll(foundConflicts);
194           }
195           break;
196         }
197       }
198     }
199
200     final HashMap<PsiElement,UsageHolder> elementsToUsageHolders = sortUsages(usages);
201     final Collection<UsageHolder> usageHolders = elementsToUsageHolders.values();
202     for (UsageHolder usageHolder : usageHolders) {
203       if (usageHolder.hasUnsafeUsagesInCode()) {
204         conflicts.add(usageHolder.getDescription());
205       }
206     }
207
208     if (!conflicts.isEmpty()) {
209       final RefactoringEventData conflictData = new RefactoringEventData();
210       conflictData.putUserData(RefactoringEventData.CONFLICTS_KEY, conflicts);
211       myProject.getMessageBus().syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC).conflictsDetected("refactoring.safeDelete", conflictData);
212       if (ApplicationManager.getApplication().isUnitTestMode()) {
213         if (!ConflictsInTestsException.isTestIgnore()) throw new ConflictsInTestsException(conflicts);
214       }
215       else {
216         UnsafeUsagesDialog dialog = new UnsafeUsagesDialog(ArrayUtil.toStringArray(conflicts), myProject);
217         if (!dialog.showAndGet()) {
218           final int exitCode = dialog.getExitCode();
219           prepareSuccessful(); // dialog is always dismissed;
220           if (exitCode == UnsafeUsagesDialog.VIEW_USAGES_EXIT_CODE) {
221             showUsages(usages);
222           }
223           return false;
224         }
225         else {
226           myPreviewNonCodeUsages = false;
227         }
228       }
229     }
230
231     UsageInfo[] preprocessedUsages = usages;
232     for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
233       preprocessedUsages = delegate.preprocessUsages(myProject, preprocessedUsages);
234       if (preprocessedUsages == null) return false;
235     }
236     final UsageInfo[] filteredUsages = UsageViewUtil.removeDuplicatedUsages(preprocessedUsages);
237     prepareSuccessful(); // dialog is always dismissed
238     if(filteredUsages == null) {
239       return false;
240     }
241     refUsages.set(filteredUsages);
242     return true;
243   }
244
245   private void showUsages(final UsageInfo[] usages) {
246     UsageViewPresentation presentation = new UsageViewPresentation();
247     presentation.setTabText(RefactoringBundle.message("safe.delete.title"));
248     presentation.setTargetsNodeText(RefactoringBundle.message("attempting.to.delete.targets.node.text"));
249     presentation.setShowReadOnlyStatusAsRed(true);
250     presentation.setShowCancelButton(true);
251     presentation.setCodeUsagesString(RefactoringBundle.message("references.found.in.code"));
252     presentation.setUsagesInGeneratedCodeString(RefactoringBundle.message("references.found.in.generated.code"));
253     presentation.setNonCodeUsagesString(RefactoringBundle.message("occurrences.found.in.comments.strings.and.non.java.files"));
254     presentation.setUsagesString(RefactoringBundle.message("usageView.usagesText"));
255
256     UsageViewManager manager = UsageViewManager.getInstance(myProject);
257     final UsageView usageView = showUsages(usages, presentation, manager);
258     usageView.addPerformOperationAction(new RerunSafeDelete(myProject, myElements, usageView),
259                                         RefactoringBundle.message("retry.command"), null, RefactoringBundle.message("rerun.safe.delete"));
260     usageView.addPerformOperationAction(new Runnable() {
261       @Override
262       public void run() {
263         UsageInfo[] preprocessedUsages = usages;
264         for (SafeDeleteProcessorDelegate delegate : Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
265           preprocessedUsages = delegate.preprocessUsages(myProject, preprocessedUsages);
266           if (preprocessedUsages == null) return;
267         }
268         final UsageInfo[] filteredUsages = UsageViewUtil.removeDuplicatedUsages(preprocessedUsages);
269         execute(filteredUsages);
270       }
271     }, "Delete Anyway", RefactoringBundle.message("usageView.need.reRun"), RefactoringBundle.message("usageView.doAction"));
272   }
273
274   private UsageView showUsages(UsageInfo[] usages, UsageViewPresentation presentation, UsageViewManager manager) {
275     for (SafeDeleteProcessorDelegate delegate : Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
276        if (delegate instanceof SafeDeleteProcessorDelegateBase) {
277          final UsageView view = ((SafeDeleteProcessorDelegateBase)delegate).showUsages(usages, presentation, manager, myElements);
278          if (view != null) return view;
279        }
280     }
281     UsageTarget[] targets = new UsageTarget[myElements.length];
282     for (int i = 0; i < targets.length; i++) {
283       targets[i] = new PsiElement2UsageTargetAdapter(myElements[i]);
284     }
285
286     return manager.showUsages(targets,
287                               UsageInfoToUsageConverter.convert(myElements, usages),
288                               presentation
289     );
290   }
291
292   public PsiElement[] getElements() {
293     return myElements;
294   }
295
296   private static class RerunSafeDelete implements Runnable {
297     final SmartPsiElementPointer[] myPointers;
298     private final Project myProject;
299     private final UsageView myUsageView;
300
301     RerunSafeDelete(Project project, PsiElement[] elements, UsageView usageView) {
302       myProject = project;
303       myUsageView = usageView;
304       myPointers = new SmartPsiElementPointer[elements.length];
305       for (int i = 0; i < elements.length; i++) {
306         PsiElement element = elements[i];
307         myPointers[i] = SmartPointerManager.getInstance(myProject).createSmartPsiElementPointer(element);
308       }
309     }
310
311     @Override
312     public void run() {
313       ApplicationManager.getApplication().invokeLater(new Runnable() {
314           @Override
315           public void run() {
316             PsiDocumentManager.getInstance(myProject).commitAllDocuments();
317             myUsageView.close();
318             ArrayList<PsiElement> elements = new ArrayList<PsiElement>();
319             for (SmartPsiElementPointer pointer : myPointers) {
320               final PsiElement element = pointer.getElement();
321               if (element != null) {
322                 elements.add(element);
323               }
324             }
325             if(!elements.isEmpty()) {
326               SafeDeleteHandler.invoke(myProject, PsiUtilCore.toPsiElementArray(elements), true);
327             }
328           }
329         });
330     }
331   }
332
333   /**
334    * @param usages
335    * @return Map from elements to UsageHolders
336    */
337   private static HashMap<PsiElement,UsageHolder> sortUsages(@NotNull UsageInfo[] usages) {
338     HashMap<PsiElement,UsageHolder> result = new HashMap<PsiElement, UsageHolder>();
339
340     for (final UsageInfo usage : usages) {
341       if (usage instanceof SafeDeleteUsageInfo) {
342         final PsiElement referencedElement = ((SafeDeleteUsageInfo)usage).getReferencedElement();
343         if (!result.containsKey(referencedElement)) {
344           result.put(referencedElement, new UsageHolder(referencedElement, usages));
345         }
346       }
347     }
348     return result;
349   }
350
351
352   @Override
353   protected void refreshElements(@NotNull PsiElement[] elements) {
354     LOG.assertTrue(elements.length == myElements.length);
355     System.arraycopy(elements, 0, myElements, 0, elements.length);
356   }
357
358   @Override
359   protected boolean isPreviewUsages(@NotNull UsageInfo[] usages) {
360     if(myPreviewNonCodeUsages && UsageViewUtil.reportNonRegularUsages(usages, myProject)) {
361       return true;
362     }
363
364     return super.isPreviewUsages(filterToBeDeleted(usages));
365   }
366
367   private static UsageInfo[] filterToBeDeleted(UsageInfo[] infos) {
368     ArrayList<UsageInfo> list = new ArrayList<UsageInfo>();
369     for (UsageInfo info : infos) {
370       if (!(info instanceof SafeDeleteReferenceUsageInfo) || ((SafeDeleteReferenceUsageInfo) info).isSafeDelete()) {
371         list.add(info);
372       }
373     }
374     return list.toArray(new UsageInfo[list.size()]);
375   }
376
377   @Nullable
378   @Override
379   protected RefactoringEventData getBeforeData() {
380     final RefactoringEventData beforeData = new RefactoringEventData();
381     beforeData.addElements(myElements);
382     return beforeData;
383   }
384
385   @Nullable
386   @Override
387   protected String getRefactoringId() {
388     return "refactoring.safeDelete";
389   }
390
391   @Override
392   protected void performRefactoring(@NotNull UsageInfo[] usages) {
393     try {
394       for (UsageInfo usage : usages) {
395         if (usage instanceof SafeDeleteCustomUsageInfo) {
396           ((SafeDeleteCustomUsageInfo) usage).performRefactoring();
397         }
398       }
399
400       DumbService.allowStartingDumbModeInside(DumbModePermission.MAY_START_MODAL, ProjectManager.getInstance().getOpenProjects(), new Runnable() {
401         @Override
402         public void run() {
403           for (PsiElement element : myElements) {
404             for (SafeDeleteProcessorDelegate delegate : Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
405               if (delegate.handlesElement(element)) {
406                 delegate.prepareForDeletion(element);
407               }
408             }
409
410             element.delete();
411           }
412         }
413       });
414     } catch (IncorrectOperationException e) {
415       RefactoringUIUtil.processIncorrectOperation(myProject, e);
416     }
417   }
418
419   private String calcCommandName() {
420     return RefactoringBundle.message("safe.delete.command", RefactoringUIUtil.calculatePsiElementDescriptionList(myElements));
421   }
422
423   private String myCachedCommandName = null;
424   @Override
425   protected String getCommandName() {
426     if (myCachedCommandName == null) {
427       myCachedCommandName = calcCommandName();
428     }
429     return myCachedCommandName;
430   }
431
432
433   public static void addNonCodeUsages(final PsiElement element,
434                                       List<UsageInfo> usages,
435                                       @Nullable final Condition<PsiElement> insideElements,
436                                       boolean searchNonJava,
437                                       boolean searchInCommentsAndStrings) {
438     UsageInfoFactory nonCodeUsageFactory = new UsageInfoFactory() {
439       @Override
440       public UsageInfo createUsageInfo(@NotNull PsiElement usage, int startOffset, int endOffset) {
441         if (insideElements != null && insideElements.value(usage)) {
442           return null;
443         }
444         return new SafeDeleteReferenceSimpleDeleteUsageInfo(usage, element, startOffset, endOffset, true, false);
445       }
446     };
447     if (searchInCommentsAndStrings) {
448       String stringToSearch = ElementDescriptionUtil.getElementDescription(element, NonCodeSearchDescriptionLocation.STRINGS_AND_COMMENTS);
449       TextOccurrencesUtil.addUsagesInStringsAndComments(element, stringToSearch, usages, nonCodeUsageFactory);
450     }
451     if (searchNonJava) {
452       String stringToSearch = ElementDescriptionUtil.getElementDescription(element, NonCodeSearchDescriptionLocation.NON_JAVA);
453       TextOccurrencesUtil.addTextOccurences(element, stringToSearch, GlobalSearchScope.projectScope(element.getProject()), usages, nonCodeUsageFactory);
454     }
455   }
456
457   @Override
458   protected boolean isToBeChanged(@NotNull UsageInfo usageInfo) {
459     if (usageInfo instanceof SafeDeleteReferenceUsageInfo) {
460       return ((SafeDeleteReferenceUsageInfo)usageInfo).isSafeDelete() && super.isToBeChanged(usageInfo);
461     }
462     return super.isToBeChanged(usageInfo);
463   }
464
465   public static boolean validElement(@NotNull PsiElement element) {
466     if (element instanceof PsiFile) return true;
467     if (!element.isPhysical()) return false;
468     final RefactoringSupportProvider provider = LanguageRefactoringSupport.INSTANCE.forLanguage(element.getLanguage());
469     return provider.isSafeDeleteAvailable(element);
470   }
471
472   public static SafeDeleteProcessor createInstance(Project project, @Nullable Runnable prepareSuccessfulCallback,
473                                                    PsiElement[] elementsToDelete, boolean isSearchInComments, boolean isSearchNonJava) {
474     return new SafeDeleteProcessor(project, prepareSuccessfulCallback, elementsToDelete, isSearchInComments, isSearchNonJava);
475   }
476
477   public static SafeDeleteProcessor createInstance(Project project, @Nullable Runnable prepareSuccessfulCallBack,
478                                                    PsiElement[] elementsToDelete, boolean isSearchInComments, boolean isSearchNonJava,
479                                                    boolean askForAccessors) {
480     ArrayList<PsiElement> elements = new ArrayList<PsiElement>(Arrays.asList(elementsToDelete));
481     HashSet<PsiElement> elementsToDeleteSet = new HashSet<PsiElement>(Arrays.asList(elementsToDelete));
482
483     for (PsiElement psiElement : elementsToDelete) {
484       for(SafeDeleteProcessorDelegate delegate: Extensions.getExtensions(SafeDeleteProcessorDelegate.EP_NAME)) {
485         if (delegate.handlesElement(psiElement)) {
486           Collection<PsiElement> addedElements = delegate.getAdditionalElementsToDelete(psiElement, elementsToDeleteSet, askForAccessors);
487           if (addedElements != null) {
488             elements.addAll(addedElements);
489           }
490           break;
491         }
492       }
493     }
494
495     return new SafeDeleteProcessor(project, prepareSuccessfulCallBack,
496                                    PsiUtilCore.toPsiElementArray(elements),
497                                    isSearchInComments, isSearchNonJava);
498   }
499
500   public boolean isSearchInCommentsAndStrings() {
501     return mySearchInCommentsAndStrings;
502   }
503
504   public void setSearchInCommentsAndStrings(boolean searchInCommentsAndStrings) {
505     mySearchInCommentsAndStrings = searchInCommentsAndStrings;
506   }
507
508   public boolean isSearchNonJava() {
509     return mySearchNonJava;
510   }
511
512   public void setSearchNonJava(boolean searchNonJava) {
513     mySearchNonJava = searchNonJava;
514   }
515
516   @Override
517   protected boolean skipNonCodeUsages() {
518     return true;
519   }
520 }