0e33345a4d327d77fb004934dd54ccd172fbce89
[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(new Runnable() {
197         @Override
198         public void run() {
199           final Project project = getContext().getProject();
200           if (isDisposed() || project.isDisposed()) return;
201           SafeDeleteHandler.invoke(project, PsiUtilCore.toPsiElementArray(psiElements), false, new Runnable() {
202             @Override
203             public void run() {
204               removeElements(refElements, project, myToolWrapper);
205             }
206           });
207         }
208       });
209
210       return false; //refresh after safe delete dialog is closed
211     }
212   }
213
214   private EntryPointsManager getEntryPointsManager() {
215     return getContext().getExtension(GlobalJavaInspectionContext.CONTEXT).getEntryPointsManager(getContext().getRefManager());
216   }
217
218   class MoveToEntries extends QuickFixAction {
219     MoveToEntries(@NotNull InspectionToolWrapper toolWrapper) {
220       super(InspectionsBundle.message("inspection.dead.code.entry.point.quickfix"), null, KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), toolWrapper);
221     }
222
223     @Override
224     protected boolean applyFix(@NotNull RefEntity[] refElements) {
225       final EntryPointsManager entryPointsManager = getEntryPointsManager();
226       for (RefEntity refElement : refElements) {
227         if (refElement instanceof RefElement) {
228           entryPointsManager.addEntryPoint((RefElement)refElement, true);
229         }
230       }
231
232       return true;
233     }
234   }
235
236   class CommentOutBin extends QuickFixAction {
237     CommentOutBin(@NotNull InspectionToolWrapper toolWrapper) {
238       super(COMMENT_OUT_QUICK_FIX, null, KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK),
239             toolWrapper);
240     }
241
242     @Override
243     protected boolean applyFix(@NotNull RefEntity[] refElements) {
244       if (!super.applyFix(refElements)) return false;
245       List<RefElement> deletedRefs = new ArrayList<RefElement>(1);
246       for (RefEntity refElement : refElements) {
247         PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getElement() : null;
248         if (psiElement == null) continue;
249         if (getFilter().getElementProblemCount((RefJavaElement)refElement) == 0) continue;
250
251         final RefEntity owner = refElement.getOwner();
252         if (!(owner instanceof RefElement && ArrayUtil.find(refElements, owner) > -1)) {
253           commentOutDead(psiElement);
254         }
255
256         refElement.getRefManager().removeRefElement((RefElement)refElement, deletedRefs);
257       }
258
259       EntryPointsManager entryPointsManager = getEntryPointsManager();
260       for (RefElement refElement : deletedRefs) {
261         entryPointsManager.removeEntryPoint(refElement);
262       }
263
264       return true;
265     }
266   }
267
268   private static final String COMMENT_OUT_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.comment.quickfix");
269   private static class CommentOutFix implements IntentionAction {
270     private final PsiElement myElement;
271
272     private CommentOutFix(final PsiElement element) {
273       myElement = element;
274     }
275
276     @Override
277     @NotNull
278     public String getText() {
279       return COMMENT_OUT_QUICK_FIX;
280     }
281
282     @Override
283     @NotNull
284     public String getFamilyName() {
285       return getText();
286     }
287
288     @Override
289     public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
290       return true;
291     }
292
293     @Override
294     public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
295       if (myElement != null && myElement.isValid()) {
296         commentOutDead(PsiTreeUtil.getParentOfType(myElement, PsiModifierListOwner.class));
297       }
298     }
299
300     @Override
301     public boolean startInWriteAction() {
302       return true;
303     }
304   }
305   private static void commentOutDead(PsiElement psiElement) {
306     PsiFile psiFile = psiElement.getContainingFile();
307
308     if (psiFile != null) {
309       Document doc = PsiDocumentManager.getInstance(psiElement.getProject()).getDocument(psiFile);
310       if (doc != null) {
311         TextRange textRange = psiElement.getTextRange();
312         String date = DateFormatUtil.formatDateTime(new Date());
313
314         int startOffset = textRange.getStartOffset();
315         CharSequence chars = doc.getCharsSequence();
316         while (CharArrayUtil.regionMatches(chars, startOffset, InspectionsBundle.message("inspection.dead.code.comment"))) {
317           int line = doc.getLineNumber(startOffset) + 1;
318           if (line < doc.getLineCount()) {
319             startOffset = doc.getLineStartOffset(line);
320             startOffset = CharArrayUtil.shiftForward(chars, startOffset, " \t");
321           }
322         }
323
324         int endOffset = textRange.getEndOffset();
325
326         int line1 = doc.getLineNumber(startOffset);
327         int line2 = doc.getLineNumber(endOffset - 1);
328
329         if (line1 == line2) {
330           doc.insertString(startOffset, InspectionsBundle.message("inspection.dead.code.date.comment", date));
331         }
332         else {
333           for (int i = line1; i <= line2; i++) {
334             doc.insertString(doc.getLineStartOffset(i), "//");
335           }
336
337           doc.insertString(doc.getLineStartOffset(Math.min(line2 + 1, doc.getLineCount() - 1)),
338                            InspectionsBundle.message("inspection.dead.code.stop.comment", date));
339           doc.insertString(doc.getLineStartOffset(line1), InspectionsBundle.message("inspection.dead.code.start.comment", date));
340         }
341       }
342     }
343   }
344
345   @NotNull
346   @Override
347   public InspectionNode createToolNode(@NotNull GlobalInspectionContextImpl context,
348                                        @NotNull InspectionNode node,
349                                        @NotNull InspectionRVContentProvider provider,
350                                        @NotNull InspectionTreeNode parentNode,
351                                        boolean showStructure) {
352     final EntryPointsNode entryPointsNode = new EntryPointsNode(context);
353     InspectionToolWrapper dummyToolWrapper = entryPointsNode.getToolWrapper();
354     InspectionToolPresentation presentation = context.getPresentation(dummyToolWrapper);
355     presentation.updateContent();
356     provider.appendToolNodeContent(context, entryPointsNode, node, showStructure);
357     return entryPointsNode;
358   }
359
360   @Override
361   public void updateContent() {
362     getTool().checkForReachableRefs(getContext());
363     myPackageContents.clear();
364     getContext().getRefManager().iterate(new RefJavaVisitor() {
365       @Override public void visitElement(@NotNull RefEntity refEntity) {
366         if (!(refEntity instanceof RefJavaElement)) return;//dead code doesn't work with refModule | refPackage
367         RefJavaElement refElement = (RefJavaElement)refEntity;
368         if (!(getContext().getUIOptions().FILTER_RESOLVED_ITEMS && getIgnoredRefElements().contains(refElement)) && refElement.isValid() && getFilter().accepts(refElement)) {
369           String packageName = RefJavaUtil.getInstance().getPackageName(refEntity);
370           Set<RefEntity> content = myPackageContents.get(packageName);
371           if (content == null) {
372             content = new HashSet<RefEntity>();
373             myPackageContents.put(packageName, content);
374           }
375           content.add(refEntity);
376         }
377       }
378     });
379   }
380
381   @Override
382   public boolean hasReportedProblems() {
383     return !myPackageContents.isEmpty();
384   }
385
386   @NotNull
387   @Override
388   public Map<String, Set<RefEntity>> getContent() {
389     return myPackageContents;
390   }
391
392   @Override
393   public void ignoreCurrentElement(RefEntity refEntity) {
394     if (refEntity == null) return;
395     myIgnoreElements.add(refEntity);
396   }
397
398   @Override
399   public void amnesty(RefEntity refEntity) {
400     myIgnoreElements.remove(refEntity);
401   }
402
403   @Override
404   public void cleanup() {
405     super.cleanup();
406     myPackageContents.clear();
407     myIgnoreElements.clear();
408   }
409
410
411   @Override
412   public void finalCleanup() {
413     super.finalCleanup();
414   }
415
416   @Override
417   public boolean isGraphNeeded() {
418     return true;
419   }
420
421   @Override
422   public boolean isElementIgnored(final RefEntity element) {
423     return myIgnoreElements.contains(element);
424   }
425
426
427   @NotNull
428   @Override
429   public FileStatus getElementStatus(final RefEntity element) {
430     return FileStatus.NOT_CHANGED;
431   }
432
433   @Override
434   @NotNull
435   public Set<RefEntity> getIgnoredRefElements() {
436     return myIgnoreElements;
437   }
438
439   @Override
440   @Nullable
441   public IntentionAction findQuickFixes(@NotNull final CommonProblemDescriptor descriptor, final String hint) {
442     if (descriptor instanceof ProblemDescriptor) {
443       if (DELETE.equals(hint)) {
444         return new PermanentDeleteFix(((ProblemDescriptor)descriptor).getPsiElement());
445       }
446       if (COMMENT.equals(hint)) {
447         return new CommentOutFix(((ProblemDescriptor)descriptor).getPsiElement());
448       }
449     }
450     return null;
451   }
452
453
454   private static class PermanentDeleteFix implements IntentionAction {
455     private final PsiElement myElement;
456
457     private PermanentDeleteFix(final PsiElement element) {
458       myElement = element;
459     }
460
461     @Override
462     @NotNull
463     public String getText() {
464       return DELETE_QUICK_FIX;
465     }
466
467     @Override
468     @NotNull
469     public String getFamilyName() {
470       return getText();
471     }
472
473     @Override
474     public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
475       return true;
476     }
477
478     @Override
479     public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
480       if (myElement != null && myElement.isValid()) {
481         ApplicationManager.getApplication().invokeLater(new Runnable() {
482           @Override
483           public void run() {
484             SafeDeleteHandler
485               .invoke(myElement.getProject(), new PsiElement[]{PsiTreeUtil.getParentOfType(myElement, PsiModifierListOwner.class)}, false);
486           }
487         });
488       }
489     }
490
491     @Override
492     public boolean startInWriteAction() {
493       return true;
494     }
495   }
496
497   @Override
498   public JComponent getCustomPreviewPanel(RefEntity entity) {
499     final Project project = entity.getRefManager().getProject();
500     JEditorPane htmlView = new JEditorPane() {
501       @Override
502       public String getToolTipText(MouseEvent evt) {
503         int pos = viewToModel(evt.getPoint());
504         if (pos >= 0) {
505           HTMLDocument hdoc = (HTMLDocument) getDocument();
506           javax.swing.text.Element e = hdoc.getCharacterElement(pos);
507           AttributeSet a = e.getAttributes();
508
509           SimpleAttributeSet value = (SimpleAttributeSet) a.getAttribute(HTML.Tag.A);
510           if (value != null) {
511             String objectPackage = (String) value.getAttribute("qualifiedname");
512             if (objectPackage != null) {
513               return objectPackage;
514             }
515           }
516         }
517         return null;
518       }
519     };
520     htmlView.setContentType(UIUtil.HTML_MIME);
521     htmlView.setEditable(false);
522     htmlView.setOpaque(false);
523     htmlView.setBackground(UIUtil.getLabelBackground());
524     htmlView.addHyperlinkListener(new HyperlinkAdapter() {
525       @Override
526       protected void hyperlinkActivated(HyperlinkEvent e) {
527         URL url = e.getURL();
528         if (url == null) {
529           return;
530         }
531         @NonNls String ref = url.getRef();
532
533         int offset = Integer.parseInt(ref);
534         String fileURL = url.toExternalForm();
535         fileURL = fileURL.substring(0, fileURL.indexOf('#'));
536         VirtualFile vFile = VirtualFileManager.getInstance().findFileByUrl(fileURL);
537         if (vFile == null) {
538           vFile = VfsUtil.findFileByURL(url);
539         }
540         if (vFile != null) {
541           final OpenFileDescriptor descriptor = new OpenFileDescriptor(project, vFile, offset);
542           FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
543         }
544       }
545     });
546     final StyleSheet css = ((HTMLEditorKit)htmlView.getEditorKit()).getStyleSheet();
547     css.addRule("p.problem-description-group {text-indent: " + JBUI.scale(9) + "px;font-weight:bold;}");
548     css.addRule("div.problem-description {margin-left: " + JBUI.scale(9) + "px;}");
549     css.addRule("ul {margin-left:" + JBUI.scale(10) + "px;text-indent: 0}");
550     css.addRule("code {font-family:" + UIUtil.getLabelFont().getFamily()  +  "}");
551     final StringBuffer buf = new StringBuffer();
552     getComposer().compose(buf, entity, false);
553     final String text = buf.toString();
554     SingleInspectionProfilePanel.readHTML(htmlView, SingleInspectionProfilePanel.toHTML(htmlView, text, false));
555     return ScrollPaneFactory.createScrollPane(htmlView, true);
556   }
557 }