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