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