1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.codeInspection.deadCode;
4 import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
5 import com.intellij.codeInspection.*;
6 import com.intellij.codeInspection.ex.*;
7 import com.intellij.codeInspection.reference.*;
8 import com.intellij.codeInspection.ui.*;
9 import com.intellij.codeInspection.unusedSymbol.UnusedSymbolLocalInspectionBase;
10 import com.intellij.codeInspection.util.RefFilter;
11 import com.intellij.concurrency.ConcurrentCollectionFactory;
12 import com.intellij.icons.AllIcons;
13 import com.intellij.ide.util.PsiNavigationSupport;
14 import com.intellij.lang.annotation.HighlightSeverity;
15 import com.intellij.openapi.actionSystem.ActionManager;
16 import com.intellij.openapi.actionSystem.AnActionEvent;
17 import com.intellij.openapi.application.ApplicationManager;
18 import com.intellij.openapi.application.ReadAction;
19 import com.intellij.openapi.editor.Document;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.util.AtomicNotNullLazyValue;
22 import com.intellij.openapi.util.SystemInfo;
23 import com.intellij.openapi.util.TextRange;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.openapi.vfs.VfsUtil;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.openapi.vfs.VirtualFileManager;
28 import com.intellij.pom.Navigatable;
29 import com.intellij.profile.codeInspection.ui.SingleInspectionProfilePanel;
30 import com.intellij.psi.*;
31 import com.intellij.psi.util.PropertyUtilBase;
32 import com.intellij.refactoring.safeDelete.SafeDeleteHandler;
33 import com.intellij.ui.HyperlinkAdapter;
34 import com.intellij.ui.ScrollPaneFactory;
35 import com.intellij.util.ArrayUtil;
36 import com.intellij.util.VisibilityUtil;
37 import com.intellij.util.containers.ContainerUtil;
38 import com.intellij.util.text.CharArrayUtil;
39 import com.intellij.util.text.DateFormatUtil;
40 import com.intellij.util.ui.JBUI;
41 import com.intellij.util.ui.UIUtil;
42 import org.jdom.Element;
43 import org.jetbrains.annotations.NonNls;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
48 import javax.swing.event.HyperlinkEvent;
49 import javax.swing.text.AttributeSet;
50 import javax.swing.text.SimpleAttributeSet;
51 import javax.swing.text.html.HTML;
52 import javax.swing.text.html.HTMLDocument;
53 import javax.swing.text.html.HTMLEditorKit;
54 import javax.swing.text.html.StyleSheet;
55 import java.awt.event.InputEvent;
56 import java.awt.event.KeyEvent;
57 import java.awt.event.MouseEvent;
60 import java.util.function.Consumer;
61 import java.util.function.Predicate;
62 import java.util.stream.Collectors;
64 public class UnusedDeclarationPresentation extends DefaultInspectionToolPresentation {
65 private final Map<RefEntity, UnusedDeclarationHint> myFixedElements =
66 ConcurrentCollectionFactory.createMap(ContainerUtil.identityStrategy());
67 private final Set<RefEntity> myExcludedElements = ConcurrentCollectionFactory.createConcurrentSet(ContainerUtil.identityStrategy());
69 private final WeakUnreferencedFilter myFilter;
70 private DeadHTMLComposer myComposer;
71 private final AtomicNotNullLazyValue<InspectionToolWrapper> myDummyWrapper = new AtomicNotNullLazyValue<InspectionToolWrapper>() {
74 protected InspectionToolWrapper compute() {
75 InspectionToolWrapper toolWrapper = new GlobalInspectionToolWrapper(new DummyEntryPointsEP());
76 toolWrapper.initialize(myContext);
81 @NonNls private static final String DELETE = "delete";
82 @NonNls private static final String COMMENT = "comment";
84 private enum UnusedDeclarationHint {
85 COMMENT("Commented out"),
88 private final String myDescription;
90 UnusedDeclarationHint(String description) {
91 myDescription = description;
94 public String getDescription() {
99 public UnusedDeclarationPresentation(@NotNull InspectionToolWrapper toolWrapper, @NotNull GlobalInspectionContextImpl context) {
100 super(toolWrapper, context);
101 myQuickFixActions = createQuickFixes(toolWrapper);
102 myFilter = new WeakUnreferencedFilter(getTool(), getContext());
103 ((EntryPointsManagerBase)getEntryPointsManager()).setAddNonJavaEntries(getTool().ADD_NONJAVA_TO_ENTRIES);
107 public RefFilter getFilter() {
110 private static class WeakUnreferencedFilter extends UnreferencedFilter {
111 private WeakUnreferencedFilter(@NotNull UnusedDeclarationInspectionBase tool, @NotNull GlobalInspectionContextImpl context) {
112 super(tool, context);
116 public int getElementProblemCount(@NotNull final RefJavaElement refElement) {
117 final int problemCount = super.getElementProblemCount(refElement);
118 if (problemCount > - 1) return problemCount;
119 if (!((RefElementImpl)refElement).hasSuspiciousCallers() || ((RefJavaElementImpl)refElement).isSuspiciousRecursive()) return 1;
121 for (RefElement element : refElement.getInReferences()) {
122 if (refElement instanceof RefFile) return 1;
123 if (((UnusedDeclarationInspectionBase)myTool).isEntryPoint(element)) return 1;
131 private UnusedDeclarationInspectionBase getTool() {
132 return (UnusedDeclarationInspectionBase)getToolWrapper().getTool();
138 public DeadHTMLComposer getComposer() {
139 if (myComposer == null) {
140 myComposer = new DeadHTMLComposer(this);
146 public boolean isExcluded(@NotNull RefEntity entity) {
147 return myExcludedElements.contains(entity);
152 public void amnesty(@NotNull RefEntity element) {
153 myExcludedElements.remove(element);
157 public void exclude(@NotNull RefEntity element) {
158 myExcludedElements.add(element);
162 public void exportResults(@NotNull Consumer<? super Element> resultConsumer,
163 @NotNull RefEntity refEntity,
164 @NotNull Predicate<? super CommonProblemDescriptor> excludedDescriptions) {
165 if (!(refEntity instanceof RefJavaElement)) return;
166 final RefFilter filter = getFilter();
167 if (!myFixedElements.containsKey(refEntity) && filter.accepts((RefJavaElement)refEntity)) {
168 refEntity = getRefManager().getRefinedElement(refEntity);
169 if (!refEntity.isValid()) return;
170 RefJavaElement refElement = (RefJavaElement)refEntity;
171 if (!compareVisibilities(refElement, getTool().getSharedLocalInspectionTool())) return;
172 if (skipEntryPoints(refElement)) return;
174 Element element = refEntity.getRefManager().export(refEntity, -1);
175 if (element == null) return;
176 @NonNls Element problemClassElement = new Element(InspectionsBundle.message("inspection.export.results.problem.element.tag"));
178 final HighlightSeverity severity = getSeverity(refElement);
179 final String attributeKey = HighlightInfoType.UNUSED_SYMBOL.getAttributesKey().getExternalName();
180 problemClassElement.setAttribute("severity", severity.myName);
181 problemClassElement.setAttribute("attribute_key", attributeKey);
183 problemClassElement.addContent(InspectionsBundle.message("inspection.export.results.dead.code"));
184 element.addContent(problemClassElement);
186 @NonNls Element hintsElement = new Element("hints");
188 for (UnusedDeclarationHint hint : UnusedDeclarationHint.values()) {
189 @NonNls Element hintElement = new Element("hint");
190 hintElement.setAttribute("value", StringUtil.toLowerCase(hint.toString()));
191 hintsElement.addContent(hintElement);
193 element.addContent(hintsElement);
196 Element descriptionElement = new Element(InspectionsBundle.message("inspection.export.results.description.tag"));
197 StringBuffer buf = new StringBuffer();
198 DeadHTMLComposer.appendProblemSynopsis((RefElement)refEntity, buf);
199 descriptionElement.addContent(buf.toString());
200 element.addContent(descriptionElement);
201 resultConsumer.accept(element);
203 super.exportResults(resultConsumer, refEntity, excludedDescriptions);
208 public QuickFixAction[] getQuickFixes(@NotNull RefEntity... refElements) {
209 return Arrays.stream(refElements).anyMatch(element -> element instanceof RefJavaElement && getFilter().accepts((RefJavaElement)element) && !myFixedElements.containsKey(element) && element.isValid())
211 : QuickFixAction.EMPTY;
214 final QuickFixAction[] myQuickFixActions;
217 private QuickFixAction[] createQuickFixes(@NotNull InspectionToolWrapper toolWrapper) {
218 return new QuickFixAction[]{new PermanentDeleteAction(toolWrapper), new CommentOutBin(toolWrapper), new MoveToEntries(toolWrapper)};
220 private static final String DELETE_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.safe.delete.quickfix");
222 class PermanentDeleteAction extends QuickFixAction {
223 PermanentDeleteAction(@NotNull InspectionToolWrapper toolWrapper) {
224 super(DELETE_QUICK_FIX, AllIcons.Actions.Cancel, null, toolWrapper);
225 copyShortcutFrom(ActionManager.getInstance().getAction("SafeDelete"));
229 protected boolean applyFix(@NotNull final RefEntity[] refElements) {
230 if (!super.applyFix(refElements)) return false;
232 //filter only elements applicable to be deleted (exclude entry points)
233 RefElement[] filteredRefElements = Arrays.stream(refElements)
234 .filter(entry -> entry instanceof RefJavaElement && getFilter().accepts((RefJavaElement)entry))
235 .toArray(RefElement[]::new);
237 ApplicationManager.getApplication().invokeLater(() -> {
238 final Project project = getContext().getProject();
239 if (isDisposed() || project.isDisposed()) return;
240 Set<RefEntity> classes = Arrays.stream(filteredRefElements)
241 .filter(refElement -> refElement instanceof RefClass)
242 .collect(Collectors.toSet());
244 //filter out elements inside classes to be deleted
245 PsiElement[] elements = Arrays.stream(filteredRefElements).filter(e -> {
246 RefEntity owner = e.getOwner();
247 if (owner != null && classes.contains(owner)) {
251 }).map(e -> e.getPsiElement())
252 .filter(e -> e != null)
253 .toArray(PsiElement[]::new);
254 SafeDeleteHandler.invoke(project, elements, false,
256 removeElements(filteredRefElements, project, myToolWrapper);
257 for (RefEntity ref : filteredRefElements) {
258 myFixedElements.put(ref, UnusedDeclarationHint.DELETE);
263 return false; //refresh after safe delete dialog is closed
267 private EntryPointsManager getEntryPointsManager() {
268 return getContext().getExtension(GlobalJavaInspectionContext.CONTEXT).getEntryPointsManager(getContext().getRefManager());
271 class MoveToEntries extends QuickFixAction {
272 MoveToEntries(@NotNull InspectionToolWrapper toolWrapper) {
273 super(InspectionsBundle.message("inspection.dead.code.entry.point.quickfix"), null, null, toolWrapper);
277 public void update(@NotNull AnActionEvent e) {
279 if (e.getPresentation().isEnabledAndVisible()) {
280 final RefEntity[] elements = getInvoker(e).getTree().getSelectedElements();
281 for (RefEntity element : elements) {
282 if (!((RefElement) element).isEntry()) {
286 e.getPresentation().setEnabled(false);
291 protected boolean applyFix(@NotNull RefEntity[] refElements) {
292 final EntryPointsManager entryPointsManager = getEntryPointsManager();
293 for (RefEntity refElement : refElements) {
294 if (refElement instanceof RefElement) {
295 entryPointsManager.addEntryPoint((RefElement)refElement, true);
303 class CommentOutBin extends QuickFixAction {
304 CommentOutBin(@NotNull InspectionToolWrapper toolWrapper) {
305 super(COMMENT_OUT_QUICK_FIX, null, KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK),
310 protected boolean applyFix(@NotNull RefEntity[] refElements) {
311 if (!super.applyFix(refElements)) return false;
312 List<RefElement> deletedRefs = new ArrayList<>(1);
313 final RefFilter filter = getFilter();
314 for (RefEntity refElement : refElements) {
315 PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getPsiElement() : null;
316 if (psiElement == null) continue;
317 if (filter.getElementProblemCount((RefJavaElement)refElement) == 0) continue;
319 final RefEntity owner = refElement.getOwner();
320 if (!(owner instanceof RefJavaElement) || filter.getElementProblemCount((RefJavaElement)owner) == 0 || !(ArrayUtil.find(refElements, owner) > -1)) {
321 commentOutDead(psiElement);
324 refElement.getRefManager().removeRefElement((RefElement)refElement, deletedRefs);
327 EntryPointsManager entryPointsManager = getEntryPointsManager();
328 for (RefElement refElement : deletedRefs) {
329 entryPointsManager.removeEntryPoint(refElement);
332 for (RefElement ref : deletedRefs) {
333 myFixedElements.put(ref, UnusedDeclarationHint.COMMENT);
339 private static final String COMMENT_OUT_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.comment.quickfix");
340 private static class CommentOutFix implements QuickFix {
341 private final RefElement myElement;
343 private CommentOutFix(RefElement element) {
349 public String getFamilyName() {
350 return COMMENT_OUT_QUICK_FIX;
354 public void applyFix(@NotNull Project project, @NotNull CommonProblemDescriptor descriptor) {
355 if (myElement != null) {
356 PsiElement element = myElement.getPsiElement();
357 if (element != null) {
358 commentOutDead(element);
364 public boolean startInWriteAction() {
368 private static void commentOutDead(PsiElement psiElement) {
369 PsiFile psiFile = psiElement.getContainingFile();
371 if (psiFile != null) {
372 Document doc = PsiDocumentManager.getInstance(psiElement.getProject()).getDocument(psiFile);
374 TextRange textRange = psiElement.getTextRange();
375 String date = DateFormatUtil.formatDateTime(new Date());
377 int startOffset = textRange.getStartOffset();
378 CharSequence chars = doc.getCharsSequence();
379 while (CharArrayUtil.regionMatches(chars, startOffset, InspectionsBundle.message("inspection.dead.code.comment"))) {
380 int line = doc.getLineNumber(startOffset) + 1;
381 if (line < doc.getLineCount()) {
382 startOffset = doc.getLineStartOffset(line);
383 startOffset = CharArrayUtil.shiftForward(chars, startOffset, " \t");
387 int endOffset = textRange.getEndOffset();
389 int line1 = doc.getLineNumber(startOffset);
390 int line2 = doc.getLineNumber(endOffset - 1);
392 if (line1 == line2) {
393 doc.insertString(startOffset, InspectionsBundle.message("inspection.dead.code.date.comment", date));
396 for (int i = line1; i <= line2; i++) {
397 doc.insertString(doc.getLineStartOffset(i), "//");
400 doc.insertString(doc.getLineStartOffset(Math.min(line2 + 1, doc.getLineCount() - 1)),
401 InspectionsBundle.message("inspection.dead.code.stop.comment", date));
402 doc.insertString(doc.getLineStartOffset(line1), InspectionsBundle.message("inspection.dead.code.start.comment", date));
409 public void patchToolNode(@NotNull InspectionTreeNode node,
410 @NotNull InspectionRVContentProvider provider,
411 boolean showStructure,
412 boolean groupByStructure) {
413 InspectionTreeModel model = myContext.getView().getTree().getInspectionTreeModel();
414 EntryPointsNode epNode = model.createCustomNode(myDummyWrapper.getValue(), () -> new EntryPointsNode(myDummyWrapper.getValue(), myContext, node), node);
415 InspectionToolPresentation presentation = myContext.getPresentation(myDummyWrapper.getValue());
416 presentation.updateContent();
417 provider.appendToolNodeContent(myContext, myDummyWrapper.getValue(), epNode, showStructure, groupByStructure);
422 public RefElementNode createRefNode(@Nullable RefEntity entity, @NotNull InspectionTreeModel model, @NotNull InspectionTreeNode parent) {
423 return new RefElementNode(entity, this, parent) {
426 public String getTailText() {
427 final UnusedDeclarationHint hint = myFixedElements.get(getElement());
429 return hint.getDescription();
431 return super.getTailText();
435 public boolean isQuickFixAppliedFromView() {
436 return myFixedElements.containsKey(getElement());
442 public boolean isProblemResolved(@Nullable RefEntity entity) {
443 return myFixedElements.containsKey(entity);
447 public synchronized void updateContent() {
448 getTool().checkForReachableRefs(getContext());
450 final UnusedSymbolLocalInspectionBase localInspectionTool = getTool().getSharedLocalInspectionTool();
451 getContext().getRefManager().iterate(new RefJavaVisitor() {
452 @Override public void visitElement(@NotNull RefEntity refEntity) {
453 if (!(refEntity instanceof RefJavaElement)) return;//dead code doesn't work with refModule | refPackage
454 RefJavaElement refElement = (RefJavaElement)refEntity;
455 if (!compareVisibilities(refElement, localInspectionTool)) return;
456 if (!(getContext().getUIOptions().FILTER_RESOLVED_ITEMS &&
457 (myFixedElements.containsKey(refElement) ||
458 isExcluded(refEntity) ||
459 isSuppressed(refElement))) && refElement.isValid() && getFilter().accepts(refElement)) {
460 if (skipEntryPoints(refElement)) return;
461 registerContentEntry(refEntity, RefJavaUtil.getInstance().getPackageName(refEntity));
465 updateProblemElements();
468 protected boolean skipEntryPoints(RefJavaElement refElement) {
469 return getTool().isEntryPoint(refElement);
472 @PsiModifier.ModifierConstant
473 private static String getAcceptedVisibility(UnusedSymbolLocalInspectionBase tool, RefJavaElement element) {
474 if (element instanceof RefImplicitConstructor) {
475 element = ((RefImplicitConstructor)element).getOwnerClass();
477 if (element instanceof RefClass) {
478 return element.getOwner() instanceof RefClass ? tool.getInnerClassVisibility() : tool.getClassVisibility();
480 if (element instanceof RefField) {
481 return tool.getFieldVisibility();
483 if (element instanceof RefMethod) {
484 final String methodVisibility = tool.getMethodVisibility();
485 if (methodVisibility != null &&
486 //todo store in the graph
487 tool.isIgnoreAccessors()) {
488 final PsiElement psi = element.getPsiElement();
489 if (psi instanceof PsiMethod && PropertyUtilBase.isSimplePropertyAccessor((PsiMethod)psi)) {
493 return methodVisibility;
495 if (element instanceof RefParameter) {
496 return tool.getParameterVisibility();
498 return PsiModifier.PUBLIC;
501 protected static boolean compareVisibilities(RefJavaElement listOwner,
502 UnusedSymbolLocalInspectionBase localInspectionTool) {
503 return compareVisibilities(listOwner, getAcceptedVisibility(localInspectionTool, listOwner));
506 protected static boolean compareVisibilities(RefJavaElement listOwner, final String acceptedVisibility) {
507 if (acceptedVisibility != null) {
508 while (listOwner != null) {
509 if (VisibilityUtil.compare(listOwner.getAccessModifier(), acceptedVisibility) >= 0) {
512 final RefEntity parent = listOwner.getOwner();
513 if (parent instanceof RefJavaElement) {
514 listOwner = (RefJavaElement)parent;
525 public void ignoreElement(@NotNull RefEntity refEntity) {
526 RefEntity owner = refEntity;
527 if (refEntity instanceof RefParameter) {
528 owner = refEntity.getOwner();
531 final CommonProblemDescriptor[] descriptors = getProblemElements().get(owner);
532 if (descriptors != null) {
533 final PsiElement psiElement = ReadAction.compute(() -> ((RefElement)refEntity).getPsiElement());
534 List<CommonProblemDescriptor> foreignDescriptors = new ArrayList<>();
535 for (CommonProblemDescriptor descriptor : descriptors) {
536 if (descriptor instanceof ProblemDescriptor) {
537 PsiElement problemElement = ReadAction.compute(() -> {
538 PsiElement element = ((ProblemDescriptor)descriptor).getPsiElement();
539 if (element instanceof PsiIdentifier) element = element.getParent();
542 if (problemElement == psiElement) continue;
544 foreignDescriptors.add(descriptor);
546 if (foreignDescriptors.size() == descriptors.length) return;
548 super.ignoreElement(owner);
552 public void cleanup() {
554 myFixedElements.clear();
559 public QuickFix findQuickFixes(@NotNull final CommonProblemDescriptor descriptor,
562 if (entity instanceof RefElement) {
563 if (DELETE.equals(hint)) {
564 return new PermanentDeleteFix((RefElement)entity);
566 if (COMMENT.equals(hint)) {
567 return new CommentOutFix((RefElement)entity);
569 if (entity instanceof RefParameter) {
570 return super.findQuickFixes(descriptor, entity, hint);
576 private static class PermanentDeleteFix implements QuickFix {
577 private final RefElement myElement;
579 private PermanentDeleteFix(@Nullable RefElement element) {
585 public String getFamilyName() {
586 return DELETE_QUICK_FIX;
591 public void applyFix(@NotNull Project project, @NotNull CommonProblemDescriptor descriptor) {
592 if (myElement != null && myElement.isValid()) {
593 SafeDeleteHandler.invoke(project, new PsiElement[]{myElement.getPsiElement()}, false);
598 public boolean startInWriteAction() {
605 public JComponent getCustomPreviewPanel(@NotNull RefEntity entity) {
606 final Project project = entity.getRefManager().getProject();
607 JEditorPane htmlView = new JEditorPane() {
609 public String getToolTipText(MouseEvent evt) {
610 int pos = viewToModel(evt.getPoint());
612 HTMLDocument hdoc = (HTMLDocument) getDocument();
613 javax.swing.text.Element e = hdoc.getCharacterElement(pos);
614 AttributeSet a = e.getAttributes();
616 SimpleAttributeSet value = (SimpleAttributeSet) a.getAttribute(HTML.Tag.A);
618 String objectPackage = (String) value.getAttribute("qualifiedname");
619 if (objectPackage != null) {
620 return objectPackage;
627 htmlView.setContentType(UIUtil.HTML_MIME);
628 htmlView.setEditable(false);
629 htmlView.setOpaque(false);
630 htmlView.setBackground(UIUtil.getLabelBackground());
631 htmlView.addHyperlinkListener(new HyperlinkAdapter() {
633 protected void hyperlinkActivated(HyperlinkEvent e) {
634 URL url = e.getURL();
638 @NonNls String ref = url.getRef();
640 int offset = Integer.parseInt(ref);
641 String fileURL = url.toExternalForm();
642 fileURL = fileURL.substring(0, fileURL.indexOf('#'));
643 VirtualFile vFile = VirtualFileManager.getInstance().findFileByUrl(fileURL);
645 vFile = VfsUtil.findFileByURL(url);
648 Navigatable descriptor = PsiNavigationSupport.getInstance().createNavigatable(project, vFile, offset);
649 descriptor.navigate(true);
653 final StyleSheet css = ((HTMLEditorKit)htmlView.getEditorKit()).getStyleSheet();
654 css.addRule("p.problem-description-group {text-indent: " + JBUI.scale(9) + "px;font-weight:bold;}");
655 css.addRule("div.problem-description {margin-left: " + JBUI.scale(9) + "px;}");
656 css.addRule("ul {margin-left:" + JBUI.scale(10) + "px;text-indent: 0}");
657 css.addRule("code {font-family:" + UIUtil.getLabelFont().getFamily() + "}");
658 final StringBuffer buf = new StringBuffer();
659 getComposer().compose(buf, entity, false);
660 final String text = buf.toString();
661 SingleInspectionProfilePanel.readHTML(htmlView, SingleInspectionProfilePanel.toHTML(htmlView, text, false));
662 return ScrollPaneFactory.createScrollPane(htmlView, true);
666 public boolean showProblemCount() {