639004bf4586e9b16af7aa704cab40aac3956fb0
[idea/community.git] / java / java-impl / src / com / intellij / codeInspection / deadCode / UnusedDeclarationPresentation.java
1 /*
2  * Copyright 2000-2016 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 package com.intellij.codeInspection.deadCode;
17
18 import com.intellij.codeInsight.intention.IntentionAction;
19 import com.intellij.codeInspection.*;
20 import com.intellij.codeInspection.ex.*;
21 import com.intellij.codeInspection.reference.*;
22 import com.intellij.codeInspection.ui.*;
23 import com.intellij.codeInspection.util.RefFilter;
24 import com.intellij.icons.AllIcons;
25 import com.intellij.lang.annotation.HighlightSeverity;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.editor.Document;
28 import com.intellij.openapi.editor.Editor;
29 import com.intellij.openapi.fileEditor.FileEditorManager;
30 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.util.SystemInfo;
33 import com.intellij.openapi.util.TextRange;
34 import com.intellij.openapi.vcs.FileStatus;
35 import com.intellij.openapi.vfs.VfsUtil;
36 import com.intellij.openapi.vfs.VirtualFile;
37 import com.intellij.openapi.vfs.VirtualFileManager;
38 import com.intellij.profile.codeInspection.ui.SingleInspectionProfilePanel;
39 import com.intellij.psi.PsiDocumentManager;
40 import com.intellij.psi.PsiElement;
41 import com.intellij.psi.PsiFile;
42 import com.intellij.psi.PsiModifierListOwner;
43 import com.intellij.psi.util.PsiTreeUtil;
44 import com.intellij.psi.util.PsiUtilCore;
45 import com.intellij.refactoring.safeDelete.SafeDeleteHandler;
46 import com.intellij.ui.HyperlinkAdapter;
47 import com.intellij.ui.ScrollPaneFactory;
48 import com.intellij.util.IncorrectOperationException;
49 import com.intellij.util.containers.HashMap;
50 import com.intellij.util.containers.HashSet;
51 import com.intellij.util.text.CharArrayUtil;
52 import com.intellij.util.text.DateFormatUtil;
53 import com.intellij.util.ui.JBUI;
54 import com.intellij.util.ui.UIUtil;
55 import org.jdom.Element;
56 import org.jetbrains.annotations.NonNls;
57 import org.jetbrains.annotations.NotNull;
58 import org.jetbrains.annotations.Nullable;
59
60 import javax.swing.*;
61 import javax.swing.event.HyperlinkEvent;
62 import javax.swing.text.AttributeSet;
63 import javax.swing.text.SimpleAttributeSet;
64 import javax.swing.text.html.HTML;
65 import javax.swing.text.html.HTMLDocument;
66 import javax.swing.text.html.HTMLEditorKit;
67 import javax.swing.text.html.StyleSheet;
68 import java.awt.event.InputEvent;
69 import java.awt.event.KeyEvent;
70 import java.awt.event.MouseEvent;
71 import java.net.URL;
72 import java.util.*;
73
74 public class UnusedDeclarationPresentation extends DefaultInspectionToolPresentation {
75   private final Map<String, Set<RefEntity>> myPackageContents = Collections.synchronizedMap(new HashMap<String, Set<RefEntity>>());
76
77   private final Set<RefEntity> myIgnoreElements = new HashSet<RefEntity>();
78   private WeakUnreferencedFilter myFilter;
79   private DeadHTMLComposer myComposer;
80   @NonNls private static final String DELETE = "delete";
81   @NonNls private static final String COMMENT = "comment";
82   @NonNls private static final String [] HINTS = {COMMENT, DELETE};
83
84   public UnusedDeclarationPresentation(@NotNull InspectionToolWrapper toolWrapper, @NotNull GlobalInspectionContextImpl context) {
85     super(toolWrapper, context);
86     myQuickFixActions = createQuickFixes(toolWrapper);
87     ((EntryPointsManagerBase)getEntryPointsManager()).setAddNonJavaEntries(getTool().ADD_NONJAVA_TO_ENTRIES);
88   }
89
90   public RefFilter getFilter() {
91     if (myFilter == null) {
92       myFilter = new WeakUnreferencedFilter(getTool(), getContext());
93     }
94     return myFilter;
95   }
96   private static class WeakUnreferencedFilter extends UnreferencedFilter {
97     private WeakUnreferencedFilter(@NotNull UnusedDeclarationInspectionBase tool, @NotNull GlobalInspectionContextImpl context) {
98       super(tool, context);
99     }
100
101     @Override
102     public int getElementProblemCount(@NotNull final RefJavaElement refElement) {
103       final int problemCount = super.getElementProblemCount(refElement);
104       if (problemCount > - 1) return problemCount;
105       if (!((RefElementImpl)refElement).hasSuspiciousCallers() || ((RefJavaElementImpl)refElement).isSuspiciousRecursive()) return 1;
106       return 0;
107     }
108   }
109
110   @NotNull
111   private UnusedDeclarationInspectionBase getTool() {
112     return (UnusedDeclarationInspectionBase)getToolWrapper().getTool();
113   }
114
115
116   @Override
117   @NotNull
118   public DeadHTMLComposer getComposer() {
119     if (myComposer == null) {
120       myComposer = new DeadHTMLComposer(this);
121     }
122     return myComposer;
123   }
124
125   @Override
126   public void exportResults(@NotNull final Element parentNode,
127                             @NotNull RefEntity refEntity,
128                             @NotNull Set<CommonProblemDescriptor> excludedDescriptions) {
129     if (!(refEntity instanceof RefJavaElement)) return;
130     final RefFilter filter = getFilter();
131     if (!getIgnoredRefElements().contains(refEntity) && filter.accepts((RefJavaElement)refEntity)) {
132       refEntity = getRefManager().getRefinedElement(refEntity);
133       Element element = refEntity.getRefManager().export(refEntity, parentNode, -1);
134       if (element == null) return;
135       @NonNls Element problemClassElement = new Element(InspectionsBundle.message("inspection.export.results.problem.element.tag"));
136
137       final RefElement refElement = (RefElement)refEntity;
138       final HighlightSeverity severity = getSeverity(refElement);
139       final String attributeKey =
140         getTextAttributeKey(refElement.getRefManager().getProject(), severity, ProblemHighlightType.LIKE_UNUSED_SYMBOL);
141       problemClassElement.setAttribute("severity", severity.myName);
142       problemClassElement.setAttribute("attribute_key", attributeKey);
143
144       problemClassElement.addContent(InspectionsBundle.message("inspection.export.results.dead.code"));
145       element.addContent(problemClassElement);
146
147       @NonNls Element hintsElement = new Element("hints");
148
149       for (String hint : HINTS) {
150         @NonNls Element hintElement = new Element("hint");
151         hintElement.setAttribute("value", hint);
152         hintsElement.addContent(hintElement);
153       }
154       element.addContent(hintsElement);
155
156
157       Element descriptionElement = new Element(InspectionsBundle.message("inspection.export.results.description.tag"));
158       StringBuffer buf = new StringBuffer();
159       DeadHTMLComposer.appendProblemSynopsis((RefElement)refEntity, buf);
160       descriptionElement.addContent(buf.toString());
161       element.addContent(descriptionElement);
162     }
163   }
164
165   @Override
166   public QuickFixAction[] getQuickFixes(@NotNull final RefEntity[] refElements, CommonProblemDescriptor[] allowedDescriptors) {
167     return myQuickFixActions;
168   }
169
170   final QuickFixAction[] myQuickFixActions;
171
172   @NotNull
173   private QuickFixAction[] createQuickFixes(@NotNull InspectionToolWrapper toolWrapper) {
174     return new QuickFixAction[]{new PermanentDeleteAction(toolWrapper), new CommentOutBin(toolWrapper), new MoveToEntries(toolWrapper)};
175   }
176   private static final String DELETE_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.safe.delete.quickfix");
177
178   class PermanentDeleteAction extends QuickFixAction {
179     PermanentDeleteAction(@NotNull InspectionToolWrapper toolWrapper) {
180       super(DELETE_QUICK_FIX, AllIcons.Actions.Cancel, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), toolWrapper);
181     }
182
183     @Override
184     protected boolean applyFix(@NotNull final RefEntity[] refElements) {
185       if (!super.applyFix(refElements)) return false;
186       final ArrayList<PsiElement> psiElements = new ArrayList<PsiElement>();
187       for (RefEntity refElement : refElements) {
188         PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getElement() : null;
189         if (psiElement == null) continue;
190         if (getFilter().getElementProblemCount((RefJavaElement)refElement) == 0) continue;
191         psiElements.add(psiElement);
192       }
193
194       ApplicationManager.getApplication().invokeLater(new Runnable() {
195         @Override
196         public void run() {
197           final Project project = getContext().getProject();
198           if (isDisposed() || project.isDisposed()) return;
199           SafeDeleteHandler.invoke(project, PsiUtilCore.toPsiElementArray(psiElements), false, new Runnable() {
200             @Override
201             public void run() {
202               removeElements(refElements, project, myToolWrapper);
203             }
204           });
205         }
206       });
207
208       return false; //refresh after safe delete dialog is closed
209     }
210   }
211
212   private EntryPointsManager getEntryPointsManager() {
213     return getContext().getExtension(GlobalJavaInspectionContext.CONTEXT).getEntryPointsManager(getContext().getRefManager());
214   }
215
216   class MoveToEntries extends QuickFixAction {
217     MoveToEntries(@NotNull InspectionToolWrapper toolWrapper) {
218       super(InspectionsBundle.message("inspection.dead.code.entry.point.quickfix"), null, KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), toolWrapper);
219     }
220
221     @Override
222     protected boolean applyFix(@NotNull RefEntity[] refElements) {
223       final EntryPointsManager entryPointsManager = getEntryPointsManager();
224       for (RefEntity refElement : refElements) {
225         if (refElement instanceof RefElement) {
226           entryPointsManager.addEntryPoint((RefElement)refElement, true);
227         }
228       }
229
230       return true;
231     }
232   }
233
234   class CommentOutBin extends QuickFixAction {
235     CommentOutBin(@NotNull InspectionToolWrapper toolWrapper) {
236       super(COMMENT_OUT_QUICK_FIX, null, KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK),
237             toolWrapper);
238     }
239
240     @Override
241     protected boolean applyFix(@NotNull RefEntity[] refElements) {
242       if (!super.applyFix(refElements)) return false;
243       List<RefElement> deletedRefs = new ArrayList<RefElement>(1);
244       for (RefEntity refElement : refElements) {
245         PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getElement() : null;
246         if (psiElement == null) continue;
247         if (getFilter().getElementProblemCount((RefJavaElement)refElement) == 0) continue;
248         commentOutDead(psiElement);
249         refElement.getRefManager().removeRefElement((RefElement)refElement, deletedRefs);
250       }
251
252       EntryPointsManager entryPointsManager = getEntryPointsManager();
253       for (RefElement refElement : deletedRefs) {
254         entryPointsManager.removeEntryPoint(refElement);
255       }
256
257       return true;
258     }
259   }
260
261   private static final String COMMENT_OUT_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.comment.quickfix");
262   private static class CommentOutFix implements IntentionAction {
263     private final PsiElement myElement;
264
265     private CommentOutFix(final PsiElement element) {
266       myElement = element;
267     }
268
269     @Override
270     @NotNull
271     public String getText() {
272       return COMMENT_OUT_QUICK_FIX;
273     }
274
275     @Override
276     @NotNull
277     public String getFamilyName() {
278       return getText();
279     }
280
281     @Override
282     public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
283       return true;
284     }
285
286     @Override
287     public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
288       if (myElement != null && myElement.isValid()) {
289         commentOutDead(PsiTreeUtil.getParentOfType(myElement, PsiModifierListOwner.class));
290       }
291     }
292
293     @Override
294     public boolean startInWriteAction() {
295       return true;
296     }
297   }
298   private static void commentOutDead(PsiElement psiElement) {
299     PsiFile psiFile = psiElement.getContainingFile();
300
301     if (psiFile != null) {
302       Document doc = PsiDocumentManager.getInstance(psiElement.getProject()).getDocument(psiFile);
303       if (doc != null) {
304         TextRange textRange = psiElement.getTextRange();
305         String date = DateFormatUtil.formatDateTime(new Date());
306
307         int startOffset = textRange.getStartOffset();
308         CharSequence chars = doc.getCharsSequence();
309         while (CharArrayUtil.regionMatches(chars, startOffset, InspectionsBundle.message("inspection.dead.code.comment"))) {
310           int line = doc.getLineNumber(startOffset) + 1;
311           if (line < doc.getLineCount()) {
312             startOffset = doc.getLineStartOffset(line);
313             startOffset = CharArrayUtil.shiftForward(chars, startOffset, " \t");
314           }
315         }
316
317         int endOffset = textRange.getEndOffset();
318
319         int line1 = doc.getLineNumber(startOffset);
320         int line2 = doc.getLineNumber(endOffset - 1);
321
322         if (line1 == line2) {
323           doc.insertString(startOffset, InspectionsBundle.message("inspection.dead.code.date.comment", date));
324         }
325         else {
326           for (int i = line1; i <= line2; i++) {
327             doc.insertString(doc.getLineStartOffset(i), "//");
328           }
329
330           doc.insertString(doc.getLineStartOffset(Math.min(line2 + 1, doc.getLineCount() - 1)),
331                            InspectionsBundle.message("inspection.dead.code.stop.comment", date));
332           doc.insertString(doc.getLineStartOffset(line1), InspectionsBundle.message("inspection.dead.code.start.comment", date));
333         }
334       }
335     }
336   }
337
338   @NotNull
339   @Override
340   public InspectionNode createToolNode(@NotNull GlobalInspectionContextImpl context,
341                                        @NotNull InspectionNode node,
342                                        @NotNull InspectionRVContentProvider provider,
343                                        @NotNull InspectionTreeNode parentNode,
344                                        boolean showStructure) {
345     final EntryPointsNode entryPointsNode = new EntryPointsNode(context);
346     InspectionToolWrapper dummyToolWrapper = entryPointsNode.getToolWrapper();
347     InspectionToolPresentation presentation = context.getPresentation(dummyToolWrapper);
348     presentation.updateContent();
349     provider.appendToolNodeContent(context, entryPointsNode, node, showStructure);
350     return entryPointsNode;
351   }
352
353   @Override
354   public void updateContent() {
355     getTool().checkForReachableRefs(getContext());
356     myPackageContents.clear();
357     getContext().getRefManager().iterate(new RefJavaVisitor() {
358       @Override public void visitElement(@NotNull RefEntity refEntity) {
359         if (!(refEntity instanceof RefJavaElement)) return;//dead code doesn't work with refModule | refPackage
360         RefJavaElement refElement = (RefJavaElement)refEntity;
361         if (!(getContext().getUIOptions().FILTER_RESOLVED_ITEMS && getIgnoredRefElements().contains(refElement)) && refElement.isValid() && getFilter().accepts(refElement)) {
362           String packageName = RefJavaUtil.getInstance().getPackageName(refEntity);
363           Set<RefEntity> content = myPackageContents.get(packageName);
364           if (content == null) {
365             content = new HashSet<RefEntity>();
366             myPackageContents.put(packageName, content);
367           }
368           content.add(refEntity);
369         }
370       }
371     });
372   }
373
374   @Override
375   public boolean hasReportedProblems() {
376     return !myPackageContents.isEmpty();
377   }
378
379   @NotNull
380   @Override
381   public Map<String, Set<RefEntity>> getContent() {
382     return myPackageContents;
383   }
384
385   @Override
386   public void ignoreCurrentElement(RefEntity refEntity) {
387     if (refEntity == null) return;
388     myIgnoreElements.add(refEntity);
389   }
390
391   @Override
392   public void amnesty(RefEntity refEntity) {
393     myIgnoreElements.remove(refEntity);
394   }
395
396   @Override
397   public void cleanup() {
398     super.cleanup();
399     myPackageContents.clear();
400     myIgnoreElements.clear();
401   }
402
403
404   @Override
405   public void finalCleanup() {
406     super.finalCleanup();
407   }
408
409   @Override
410   public boolean isGraphNeeded() {
411     return true;
412   }
413
414   @Override
415   public boolean isElementIgnored(final RefEntity element) {
416     return myIgnoreElements.contains(element);
417   }
418
419
420   @NotNull
421   @Override
422   public FileStatus getElementStatus(final RefEntity element) {
423     return FileStatus.NOT_CHANGED;
424   }
425
426   @Override
427   @NotNull
428   public Set<RefEntity> getIgnoredRefElements() {
429     return myIgnoreElements;
430   }
431
432   @Override
433   @Nullable
434   public IntentionAction findQuickFixes(@NotNull final CommonProblemDescriptor descriptor, final String hint) {
435     if (descriptor instanceof ProblemDescriptor) {
436       if (DELETE.equals(hint)) {
437         return new PermanentDeleteFix(((ProblemDescriptor)descriptor).getPsiElement());
438       }
439       if (COMMENT.equals(hint)) {
440         return new CommentOutFix(((ProblemDescriptor)descriptor).getPsiElement());
441       }
442     }
443     return null;
444   }
445
446
447   private static class PermanentDeleteFix implements IntentionAction {
448     private final PsiElement myElement;
449
450     private PermanentDeleteFix(final PsiElement element) {
451       myElement = element;
452     }
453
454     @Override
455     @NotNull
456     public String getText() {
457       return DELETE_QUICK_FIX;
458     }
459
460     @Override
461     @NotNull
462     public String getFamilyName() {
463       return getText();
464     }
465
466     @Override
467     public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
468       return true;
469     }
470
471     @Override
472     public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
473       if (myElement != null && myElement.isValid()) {
474         ApplicationManager.getApplication().invokeLater(new Runnable() {
475           @Override
476           public void run() {
477             SafeDeleteHandler
478               .invoke(myElement.getProject(), new PsiElement[]{PsiTreeUtil.getParentOfType(myElement, PsiModifierListOwner.class)}, false);
479           }
480         });
481       }
482     }
483
484     @Override
485     public boolean startInWriteAction() {
486       return true;
487     }
488   }
489
490   @Override
491   public JComponent getCustomPreviewPanel(RefEntity entity) {
492     final Project project = entity.getRefManager().getProject();
493     JEditorPane htmlView = new JEditorPane() {
494       @Override
495       public String getToolTipText(MouseEvent evt) {
496         int pos = viewToModel(evt.getPoint());
497         if (pos >= 0) {
498           HTMLDocument hdoc = (HTMLDocument) getDocument();
499           javax.swing.text.Element e = hdoc.getCharacterElement(pos);
500           AttributeSet a = e.getAttributes();
501
502           SimpleAttributeSet value = (SimpleAttributeSet) a.getAttribute(HTML.Tag.A);
503           if (value != null) {
504             String objectPackage = (String) value.getAttribute("qualifiedname");
505             if (objectPackage != null) {
506               return objectPackage;
507             }
508           }
509         }
510         return null;
511       }
512     };
513     htmlView.setContentType(UIUtil.HTML_MIME);
514     htmlView.setEditable(false);
515     htmlView.setOpaque(false);
516     htmlView.addHyperlinkListener(new HyperlinkAdapter() {
517       @Override
518       protected void hyperlinkActivated(HyperlinkEvent e) {
519         URL url = e.getURL();
520         if (url == null) {
521           return;
522         }
523         @NonNls String ref = url.getRef();
524
525         int offset = Integer.parseInt(ref);
526         String fileURL = url.toExternalForm();
527         fileURL = fileURL.substring(0, fileURL.indexOf('#'));
528         VirtualFile vFile = VirtualFileManager.getInstance().findFileByUrl(fileURL);
529         if (vFile == null) {
530           vFile = VfsUtil.findFileByURL(url);
531         }
532         if (vFile != null) {
533           final OpenFileDescriptor descriptor = new OpenFileDescriptor(project, vFile, offset);
534           FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
535         }
536       }
537     });
538     final StyleSheet css = ((HTMLEditorKit)htmlView.getEditorKit()).getStyleSheet();
539     css.addRule("p.problem-description-group {text-indent: " + JBUI.scale(9) + "px;font-weight:bold;}");
540     css.addRule("div.problem-description {margin-left: " + JBUI.scale(9) + "px;}");
541     css.addRule("ul {margin-left:" + JBUI.scale(10) + "px;text-indent: 0}");
542     css.addRule("code {font-family:" + UIUtil.getLabelFont().getFamily()  +  "}");
543     final StringBuffer buf = new StringBuffer();
544     getComposer().compose(buf, entity, false);
545     final String text = buf.toString();
546     SingleInspectionProfilePanel.readHTML(htmlView, SingleInspectionProfilePanel.toHTML(htmlView, text, false));
547     return ScrollPaneFactory.createScrollPane(htmlView, true);
548   }
549 }