2 * Copyright 2000-2016 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.codeInspection.deadCode;
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;
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;
77 import java.util.function.Predicate;
79 public class UnusedDeclarationPresentation extends DefaultInspectionToolPresentation {
80 private final Map<String, Set<RefEntity>> myPackageContents = Collections.synchronizedMap(new HashMap<String, Set<RefEntity>>());
82 private final Set<RefEntity> myIgnoreElements = ContainerUtil.newConcurrentSet(TObjectHashingStrategy.IDENTITY);
83 private final Map<RefEntity, UnusedDeclarationHint> myFixedElements = ContainerUtil.newConcurrentMap(TObjectHashingStrategy.IDENTITY);
85 private WeakUnreferencedFilter myFilter;
86 private DeadHTMLComposer myComposer;
87 @NonNls private static final String DELETE = "delete";
88 @NonNls private static final String COMMENT = "comment";
90 private enum UnusedDeclarationHint {
91 COMMENT("Commented out"),
94 private final String myDescription;
96 UnusedDeclarationHint(String description) {
97 myDescription = description;
100 public String getDescription() {
101 return myDescription;
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);
111 public RefFilter getFilter() {
112 if (myFilter == null) {
113 myFilter = new WeakUnreferencedFilter(getTool(), getContext());
117 private static class WeakUnreferencedFilter extends UnreferencedFilter {
118 private WeakUnreferencedFilter(@NotNull UnusedDeclarationInspectionBase tool, @NotNull GlobalInspectionContextImpl context) {
119 super(tool, context);
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;
132 private UnusedDeclarationInspectionBase getTool() {
133 return (UnusedDeclarationInspectionBase)getToolWrapper().getTool();
139 public DeadHTMLComposer getComposer() {
140 if (myComposer == null) {
141 myComposer = new DeadHTMLComposer(this);
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"));
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);
166 problemClassElement.addContent(InspectionsBundle.message("inspection.export.results.dead.code"));
167 element.addContent(problemClassElement);
169 @NonNls Element hintsElement = new Element("hints");
171 for (UnusedDeclarationHint hint : UnusedDeclarationHint.values()) {
172 @NonNls Element hintElement = new Element("hint");
173 hintElement.setAttribute("value", hint.toString().toLowerCase());
174 hintsElement.addContent(hintElement);
176 element.addContent(hintsElement);
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);
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()) {
197 return showFixes ? myQuickFixActions : QuickFixAction.EMPTY;
200 final QuickFixAction[] myQuickFixActions;
203 private QuickFixAction[] createQuickFixes(@NotNull InspectionToolWrapper toolWrapper) {
204 return new QuickFixAction[]{new PermanentDeleteAction(toolWrapper), new CommentOutBin(toolWrapper), new MoveToEntries(toolWrapper)};
206 private static final String DELETE_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.safe.delete.quickfix");
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"));
215 protected boolean applyFix(@NotNull final RefEntity[] refElements) {
216 if (!super.applyFix(refElements)) return false;
217 final PsiElement[] psiElements = Arrays
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,
228 removeElements(refElements, project, myToolWrapper);
229 for (RefEntity ref : refElements) {
230 myFixedElements.put(ref, UnusedDeclarationHint.DELETE);
235 return false; //refresh after safe delete dialog is closed
239 private EntryPointsManager getEntryPointsManager() {
240 return getContext().getExtension(GlobalJavaInspectionContext.CONTEXT).getEntryPointsManager(getContext().getRefManager());
243 class MoveToEntries extends QuickFixAction {
244 MoveToEntries(@NotNull InspectionToolWrapper toolWrapper) {
245 super(InspectionsBundle.message("inspection.dead.code.entry.point.quickfix"), null, null, toolWrapper);
249 public void update(AnActionEvent e) {
251 if (e.getPresentation().isEnabledAndVisible()) {
252 final RefEntity[] elements = getInvoker(e).getTree().getSelectedElements();
253 for (RefEntity element : elements) {
254 if (!((RefElement) element).isEntry()) {
258 e.getPresentation().setEnabled(false);
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);
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),
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;
291 final RefEntity owner = refElement.getOwner();
292 if (!(owner instanceof RefJavaElement) || filter.getElementProblemCount((RefJavaElement)owner) == 0 || !(ArrayUtil.find(refElements, owner) > -1)) {
293 commentOutDead(psiElement);
296 refElement.getRefManager().removeRefElement((RefElement)refElement, deletedRefs);
299 EntryPointsManager entryPointsManager = getEntryPointsManager();
300 for (RefElement refElement : deletedRefs) {
301 entryPointsManager.removeEntryPoint(refElement);
304 for (RefElement ref : deletedRefs) {
305 myFixedElements.put(ref, UnusedDeclarationHint.COMMENT);
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;
315 private CommentOutFix(final PsiElement element) {
321 public String getText() {
322 return COMMENT_OUT_QUICK_FIX;
327 public String getFamilyName() {
332 public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
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));
344 public boolean startInWriteAction() {
348 private static void commentOutDead(PsiElement psiElement) {
349 PsiFile psiFile = psiElement.getContainingFile();
351 if (psiFile != null) {
352 Document doc = PsiDocumentManager.getInstance(psiElement.getProject()).getDocument(psiFile);
354 TextRange textRange = psiElement.getTextRange();
355 String date = DateFormatUtil.formatDateTime(new Date());
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");
367 int endOffset = textRange.getEndOffset();
369 int line1 = doc.getLineNumber(startOffset);
370 int line2 = doc.getLineNumber(endOffset - 1);
372 if (line1 == line2) {
373 doc.insertString(startOffset, InspectionsBundle.message("inspection.dead.code.date.comment", date));
376 for (int i = line1; i <= line2; i++) {
377 doc.insertString(doc.getLineStartOffset(i), "//");
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));
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;
406 public RefElementNode createRefNode(@NotNull RefEntity entity) {
407 return new RefElementNode(entity, this) {
410 public String getCustomizedTailText() {
411 final UnusedDeclarationHint hint = myFixedElements.get(getElement());
413 return hint.getDescription();
415 return super.getCustomizedTailText();
419 public boolean isQuickFixAppliedFromView() {
420 return myFixedElements.containsKey(getElement());
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);
440 content.add(refEntity);
447 public boolean hasReportedProblems() {
448 return !myPackageContents.isEmpty();
453 public Map<String, Set<RefEntity>> getContent() {
454 return myPackageContents;
458 public void ignoreCurrentElement(RefEntity refEntity) {
459 if (refEntity == null) return;
460 myIgnoreElements.add(refEntity);
464 public void amnesty(RefEntity refEntity) {
465 myIgnoreElements.remove(refEntity);
469 public void cleanup() {
471 myPackageContents.clear();
472 myIgnoreElements.clear();
477 public void finalCleanup() {
478 super.finalCleanup();
482 public boolean isGraphNeeded() {
487 public boolean isElementIgnored(final RefEntity element) {
488 return myIgnoreElements.contains(element);
494 public FileStatus getElementStatus(final RefEntity element) {
495 return FileStatus.NOT_CHANGED;
500 public Set<RefEntity> getIgnoredRefElements() {
501 return myIgnoreElements;
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());
511 if (COMMENT.equals(hint)) {
512 return new CommentOutFix(((ProblemDescriptor)descriptor).getPsiElement());
519 private static class PermanentDeleteFix implements IntentionAction {
520 private final PsiElement myElement;
522 private PermanentDeleteFix(final PsiElement element) {
528 public String getText() {
529 return DELETE_QUICK_FIX;
534 public String getFamilyName() {
539 public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
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));
552 public boolean startInWriteAction() {
558 public JComponent getCustomPreviewPanel(RefEntity entity) {
559 final Project project = entity.getRefManager().getProject();
560 JEditorPane htmlView = new JEditorPane() {
562 public String getToolTipText(MouseEvent evt) {
563 int pos = viewToModel(evt.getPoint());
565 HTMLDocument hdoc = (HTMLDocument) getDocument();
566 javax.swing.text.Element e = hdoc.getCharacterElement(pos);
567 AttributeSet a = e.getAttributes();
569 SimpleAttributeSet value = (SimpleAttributeSet) a.getAttribute(HTML.Tag.A);
571 String objectPackage = (String) value.getAttribute("qualifiedname");
572 if (objectPackage != null) {
573 return objectPackage;
580 htmlView.setContentType(UIUtil.HTML_MIME);
581 htmlView.setEditable(false);
582 htmlView.setOpaque(false);
583 htmlView.setBackground(UIUtil.getLabelBackground());
584 htmlView.addHyperlinkListener(new HyperlinkAdapter() {
586 protected void hyperlinkActivated(HyperlinkEvent e) {
587 URL url = e.getURL();
591 @NonNls String ref = url.getRef();
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);
598 vFile = VfsUtil.findFileByURL(url);
601 final OpenFileDescriptor descriptor = new OpenFileDescriptor(project, vFile, offset);
602 FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
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);