Merge remote-tracking branch 'origin/master'
[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.ArrayUtil;
49 import com.intellij.util.IncorrectOperationException;
50 import com.intellij.util.containers.HashMap;
51 import com.intellij.util.containers.HashSet;
52 import com.intellij.util.text.CharArrayUtil;
53 import com.intellij.util.text.DateFormatUtil;
54 import com.intellij.util.ui.JBUI;
55 import com.intellij.util.ui.UIUtil;
56 import org.jdom.Element;
57 import org.jetbrains.annotations.NonNls;
58 import org.jetbrains.annotations.NotNull;
59 import org.jetbrains.annotations.Nullable;
60
61 import javax.swing.*;
62 import javax.swing.event.HyperlinkEvent;
63 import javax.swing.text.AttributeSet;
64 import javax.swing.text.SimpleAttributeSet;
65 import javax.swing.text.html.HTML;
66 import javax.swing.text.html.HTMLDocument;
67 import javax.swing.text.html.HTMLEditorKit;
68 import javax.swing.text.html.StyleSheet;
69 import java.awt.event.InputEvent;
70 import java.awt.event.KeyEvent;
71 import java.awt.event.MouseEvent;
72 import java.net.URL;
73 import java.util.*;
74 import java.util.function.Predicate;
75
76 public class UnusedDeclarationPresentation extends DefaultInspectionToolPresentation {
77   private final Map<String, Set<RefEntity>> myPackageContents = Collections.synchronizedMap(new HashMap<String, Set<RefEntity>>());
78
79   private final Set<RefEntity> myIgnoreElements = new HashSet<RefEntity>();
80   private WeakUnreferencedFilter myFilter;
81   private DeadHTMLComposer myComposer;
82   @NonNls private static final String DELETE = "delete";
83   @NonNls private static final String COMMENT = "comment";
84   @NonNls private static final String [] HINTS = {COMMENT, DELETE};
85
86   public UnusedDeclarationPresentation(@NotNull InspectionToolWrapper toolWrapper, @NotNull GlobalInspectionContextImpl context) {
87     super(toolWrapper, context);
88     myQuickFixActions = createQuickFixes(toolWrapper);
89     ((EntryPointsManagerBase)getEntryPointsManager()).setAddNonJavaEntries(getTool().ADD_NONJAVA_TO_ENTRIES);
90   }
91
92   public RefFilter getFilter() {
93     if (myFilter == null) {
94       myFilter = new WeakUnreferencedFilter(getTool(), getContext());
95     }
96     return myFilter;
97   }
98   private static class WeakUnreferencedFilter extends UnreferencedFilter {
99     private WeakUnreferencedFilter(@NotNull UnusedDeclarationInspectionBase tool, @NotNull GlobalInspectionContextImpl context) {
100       super(tool, context);
101     }
102
103     @Override
104     public int getElementProblemCount(@NotNull final RefJavaElement refElement) {
105       final int problemCount = super.getElementProblemCount(refElement);
106       if (problemCount > - 1) return problemCount;
107       if (!((RefElementImpl)refElement).hasSuspiciousCallers() || ((RefJavaElementImpl)refElement).isSuspiciousRecursive()) return 1;
108       return 0;
109     }
110   }
111
112   @NotNull
113   private UnusedDeclarationInspectionBase getTool() {
114     return (UnusedDeclarationInspectionBase)getToolWrapper().getTool();
115   }
116
117
118   @Override
119   @NotNull
120   public DeadHTMLComposer getComposer() {
121     if (myComposer == null) {
122       myComposer = new DeadHTMLComposer(this);
123     }
124     return myComposer;
125   }
126
127   @Override
128   public void exportResults(@NotNull final Element parentNode,
129                             @NotNull RefEntity refEntity,
130                             @NotNull Predicate<CommonProblemDescriptor> excludedDescriptions) {
131     if (!(refEntity instanceof RefJavaElement)) return;
132     final RefFilter filter = getFilter();
133     if (!getIgnoredRefElements().contains(refEntity) && filter.accepts((RefJavaElement)refEntity)) {
134       refEntity = getRefManager().getRefinedElement(refEntity);
135       Element element = refEntity.getRefManager().export(refEntity, parentNode, -1);
136       if (element == null) return;
137       @NonNls Element problemClassElement = new Element(InspectionsBundle.message("inspection.export.results.problem.element.tag"));
138
139       final RefElement refElement = (RefElement)refEntity;
140       final HighlightSeverity severity = getSeverity(refElement);
141       final String attributeKey =
142         getTextAttributeKey(refElement.getRefManager().getProject(), severity, ProblemHighlightType.LIKE_UNUSED_SYMBOL);
143       problemClassElement.setAttribute("severity", severity.myName);
144       problemClassElement.setAttribute("attribute_key", attributeKey);
145
146       problemClassElement.addContent(InspectionsBundle.message("inspection.export.results.dead.code"));
147       element.addContent(problemClassElement);
148
149       @NonNls Element hintsElement = new Element("hints");
150
151       for (String hint : HINTS) {
152         @NonNls Element hintElement = new Element("hint");
153         hintElement.setAttribute("value", hint);
154         hintsElement.addContent(hintElement);
155       }
156       element.addContent(hintsElement);
157
158
159       Element descriptionElement = new Element(InspectionsBundle.message("inspection.export.results.description.tag"));
160       StringBuffer buf = new StringBuffer();
161       DeadHTMLComposer.appendProblemSynopsis((RefElement)refEntity, buf);
162       descriptionElement.addContent(buf.toString());
163       element.addContent(descriptionElement);
164     }
165   }
166
167   @Override
168   public QuickFixAction[] getQuickFixes(@NotNull final RefEntity[] refElements, CommonProblemDescriptor[] allowedDescriptors) {
169     return myQuickFixActions;
170   }
171
172   final QuickFixAction[] myQuickFixActions;
173
174   @NotNull
175   private QuickFixAction[] createQuickFixes(@NotNull InspectionToolWrapper toolWrapper) {
176     return new QuickFixAction[]{new PermanentDeleteAction(toolWrapper), new CommentOutBin(toolWrapper), new MoveToEntries(toolWrapper)};
177   }
178   private static final String DELETE_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.safe.delete.quickfix");
179
180   class PermanentDeleteAction extends QuickFixAction {
181     PermanentDeleteAction(@NotNull InspectionToolWrapper toolWrapper) {
182       super(DELETE_QUICK_FIX, AllIcons.Actions.Cancel, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), toolWrapper);
183     }
184
185     @Override
186     protected boolean applyFix(@NotNull final RefEntity[] refElements) {
187       if (!super.applyFix(refElements)) return false;
188       final ArrayList<PsiElement> psiElements = new ArrayList<PsiElement>();
189       for (RefEntity refElement : refElements) {
190         PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getElement() : null;
191         if (psiElement == null) continue;
192         if (getFilter().getElementProblemCount((RefJavaElement)refElement) == 0) continue;
193         psiElements.add(psiElement);
194       }
195
196       ApplicationManager.getApplication().invokeLater(() -> {
197         final Project project = getContext().getProject();
198         if (isDisposed() || project.isDisposed()) return;
199         SafeDeleteHandler.invoke(project, PsiUtilCore.toPsiElementArray(psiElements), false,
200                                  () -> removeElements(refElements, project, myToolWrapper));
201       });
202
203       return false; //refresh after safe delete dialog is closed
204     }
205   }
206
207   private EntryPointsManager getEntryPointsManager() {
208     return getContext().getExtension(GlobalJavaInspectionContext.CONTEXT).getEntryPointsManager(getContext().getRefManager());
209   }
210
211   class MoveToEntries extends QuickFixAction {
212     MoveToEntries(@NotNull InspectionToolWrapper toolWrapper) {
213       super(InspectionsBundle.message("inspection.dead.code.entry.point.quickfix"), null, KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), toolWrapper);
214     }
215
216     @Override
217     protected boolean applyFix(@NotNull RefEntity[] refElements) {
218       final EntryPointsManager entryPointsManager = getEntryPointsManager();
219       for (RefEntity refElement : refElements) {
220         if (refElement instanceof RefElement) {
221           entryPointsManager.addEntryPoint((RefElement)refElement, true);
222         }
223       }
224
225       return true;
226     }
227   }
228
229   class CommentOutBin extends QuickFixAction {
230     CommentOutBin(@NotNull InspectionToolWrapper toolWrapper) {
231       super(COMMENT_OUT_QUICK_FIX, null, KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK),
232             toolWrapper);
233     }
234
235     @Override
236     protected boolean applyFix(@NotNull RefEntity[] refElements) {
237       if (!super.applyFix(refElements)) return false;
238       List<RefElement> deletedRefs = new ArrayList<RefElement>(1);
239       for (RefEntity refElement : refElements) {
240         PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getElement() : null;
241         if (psiElement == null) continue;
242         if (getFilter().getElementProblemCount((RefJavaElement)refElement) == 0) continue;
243
244         final RefEntity owner = refElement.getOwner();
245         if (!(owner instanceof RefElement && ArrayUtil.find(refElements, owner) > -1)) {
246           commentOutDead(psiElement);
247         }
248
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(() -> SafeDeleteHandler
475           .invoke(myElement.getProject(), new PsiElement[]{PsiTreeUtil.getParentOfType(myElement, PsiModifierListOwner.class)}, false));
476       }
477     }
478
479     @Override
480     public boolean startInWriteAction() {
481       return true;
482     }
483   }
484
485   @Override
486   public JComponent getCustomPreviewPanel(RefEntity entity) {
487     final Project project = entity.getRefManager().getProject();
488     JEditorPane htmlView = new JEditorPane() {
489       @Override
490       public String getToolTipText(MouseEvent evt) {
491         int pos = viewToModel(evt.getPoint());
492         if (pos >= 0) {
493           HTMLDocument hdoc = (HTMLDocument) getDocument();
494           javax.swing.text.Element e = hdoc.getCharacterElement(pos);
495           AttributeSet a = e.getAttributes();
496
497           SimpleAttributeSet value = (SimpleAttributeSet) a.getAttribute(HTML.Tag.A);
498           if (value != null) {
499             String objectPackage = (String) value.getAttribute("qualifiedname");
500             if (objectPackage != null) {
501               return objectPackage;
502             }
503           }
504         }
505         return null;
506       }
507     };
508     htmlView.setContentType(UIUtil.HTML_MIME);
509     htmlView.setEditable(false);
510     htmlView.setOpaque(false);
511     htmlView.setBackground(UIUtil.getLabelBackground());
512     htmlView.addHyperlinkListener(new HyperlinkAdapter() {
513       @Override
514       protected void hyperlinkActivated(HyperlinkEvent e) {
515         URL url = e.getURL();
516         if (url == null) {
517           return;
518         }
519         @NonNls String ref = url.getRef();
520
521         int offset = Integer.parseInt(ref);
522         String fileURL = url.toExternalForm();
523         fileURL = fileURL.substring(0, fileURL.indexOf('#'));
524         VirtualFile vFile = VirtualFileManager.getInstance().findFileByUrl(fileURL);
525         if (vFile == null) {
526           vFile = VfsUtil.findFileByURL(url);
527         }
528         if (vFile != null) {
529           final OpenFileDescriptor descriptor = new OpenFileDescriptor(project, vFile, offset);
530           FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
531         }
532       }
533     });
534     final StyleSheet css = ((HTMLEditorKit)htmlView.getEditorKit()).getStyleSheet();
535     css.addRule("p.problem-description-group {text-indent: " + JBUI.scale(9) + "px;font-weight:bold;}");
536     css.addRule("div.problem-description {margin-left: " + JBUI.scale(9) + "px;}");
537     css.addRule("ul {margin-left:" + JBUI.scale(10) + "px;text-indent: 0}");
538     css.addRule("code {font-family:" + UIUtil.getLabelFont().getFamily()  +  "}");
539     final StringBuffer buf = new StringBuffer();
540     getComposer().compose(buf, entity, false);
541     final String text = buf.toString();
542     SingleInspectionProfilePanel.readHTML(htmlView, SingleInspectionProfilePanel.toHTML(htmlView, text, false));
543     return ScrollPaneFactory.createScrollPane(htmlView, true);
544   }
545 }