blob: 276bb97e29229ae90384ef900cc00a518f538337 [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.deadCode;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.ex.*;
import com.intellij.codeInspection.reference.*;
import com.intellij.codeInspection.ui.*;
import com.intellij.codeInspection.util.RefFilter;
import com.intellij.icons.AllIcons;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.safeDelete.SafeDeleteHandler;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.HashMap;
import com.intellij.util.containers.HashSet;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.text.DateFormatUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.*;
public class UnusedDeclarationPresentation extends DefaultInspectionToolPresentation {
private Map<String, Set<RefEntity>> myPackageContents = new HashMap<String, Set<RefEntity>>();
private Map<String, Set<RefEntity>> myOldPackageContents = null;
private final Set<RefEntity> myIgnoreElements = new HashSet<RefEntity>();
private WeakUnreferencedFilter myFilter;
private DeadHTMLComposer myComposer;
@NonNls private static final String DELETE = "delete";
@NonNls private static final String COMMENT = "comment";
@NonNls private static final String [] HINTS = {COMMENT, DELETE};
public UnusedDeclarationPresentation(@NotNull InspectionToolWrapper toolWrapper, @NotNull GlobalInspectionContextImpl context) {
super(toolWrapper, context);
myQuickFixActions = createQuickFixes(toolWrapper);
((EntryPointsManagerBase)getEntryPointsManager()).setAddNonJavaEntries(getTool().ADD_NONJAVA_TO_ENTRIES);
}
public RefFilter getFilter() {
if (myFilter == null) {
myFilter = new WeakUnreferencedFilter(getTool(), getContext());
}
return myFilter;
}
private static class WeakUnreferencedFilter extends UnreferencedFilter {
private WeakUnreferencedFilter(@NotNull UnusedDeclarationInspection tool, @NotNull GlobalInspectionContextImpl context) {
super(tool, context);
}
@Override
public int getElementProblemCount(@NotNull final RefJavaElement refElement) {
final int problemCount = super.getElementProblemCount(refElement);
if (problemCount > - 1) return problemCount;
if (!((RefElementImpl)refElement).hasSuspiciousCallers() || ((RefJavaElementImpl)refElement).isSuspiciousRecursive()) return 1;
return 0;
}
}
@NotNull
private UnusedDeclarationInspection getTool() {
return (UnusedDeclarationInspection)getToolWrapper().getTool();
}
@Override
@NotNull
public HTMLComposerImpl getComposer() {
if (myComposer == null) {
myComposer = new DeadHTMLComposer(this);
}
return myComposer;
}
@Override
public void exportResults(@NotNull final Element parentNode, @NotNull RefEntity refEntity) {
if (!(refEntity instanceof RefJavaElement)) return;
final RefFilter filter = getFilter();
if (!getIgnoredRefElements().contains(refEntity) && filter.accepts((RefJavaElement)refEntity)) {
refEntity = getRefManager().getRefinedElement(refEntity);
Element element = refEntity.getRefManager().export(refEntity, parentNode, -1);
if (element == null) return;
@NonNls Element problemClassElement = new Element(InspectionsBundle.message("inspection.export.results.problem.element.tag"));
final RefElement refElement = (RefElement)refEntity;
final HighlightSeverity severity = getSeverity(refElement);
final String attributeKey =
getTextAttributeKey(refElement.getRefManager().getProject(), severity, ProblemHighlightType.LIKE_UNUSED_SYMBOL);
problemClassElement.setAttribute("severity", severity.myName);
problemClassElement.setAttribute("attribute_key", attributeKey);
problemClassElement.addContent(InspectionsBundle.message("inspection.export.results.dead.code"));
element.addContent(problemClassElement);
@NonNls Element hintsElement = new Element("hints");
for (String hint : HINTS) {
@NonNls Element hintElement = new Element("hint");
hintElement.setAttribute("value", hint);
hintsElement.addContent(hintElement);
}
element.addContent(hintsElement);
Element descriptionElement = new Element(InspectionsBundle.message("inspection.export.results.description.tag"));
StringBuffer buf = new StringBuffer();
DeadHTMLComposer.appendProblemSynopsis((RefElement)refEntity, buf);
descriptionElement.addContent(buf.toString());
element.addContent(descriptionElement);
}
}
@Override
public QuickFixAction[] getQuickFixes(@NotNull final RefEntity[] refElements) {
return myQuickFixActions;
}
final QuickFixAction[] myQuickFixActions;
@NotNull
private QuickFixAction[] createQuickFixes(@NotNull InspectionToolWrapper toolWrapper) {
return new QuickFixAction[]{new PermanentDeleteAction(toolWrapper), new CommentOutBin(toolWrapper), new MoveToEntries(toolWrapper)};
}
private static final String DELETE_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.safe.delete.quickfix");
class PermanentDeleteAction extends QuickFixAction {
PermanentDeleteAction(@NotNull InspectionToolWrapper toolWrapper) {
super(DELETE_QUICK_FIX, AllIcons.Actions.Cancel, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), toolWrapper);
}
@Override
protected boolean applyFix(@NotNull final RefEntity[] refElements) {
if (!super.applyFix(refElements)) return false;
final ArrayList<PsiElement> psiElements = new ArrayList<PsiElement>();
for (RefEntity refElement : refElements) {
PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getElement() : null;
if (psiElement == null) continue;
if (getFilter().getElementProblemCount((RefJavaElement)refElement) == 0) continue;
psiElements.add(psiElement);
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
final Project project = getContext().getProject();
if (isDisposed() || project.isDisposed()) return;
SafeDeleteHandler.invoke(project, PsiUtilCore.toPsiElementArray(psiElements), false, new Runnable() {
@Override
public void run() {
removeElements(refElements, project, myToolWrapper);
}
});
}
});
return false; //refresh after safe delete dialog is closed
}
}
private EntryPointsManager getEntryPointsManager() {
return getContext().getExtension(GlobalJavaInspectionContext.CONTEXT).getEntryPointsManager(getContext().getRefManager());
}
class MoveToEntries extends QuickFixAction {
MoveToEntries(@NotNull InspectionToolWrapper toolWrapper) {
super(InspectionsBundle.message("inspection.dead.code.entry.point.quickfix"), null, KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), toolWrapper);
}
@Override
protected boolean applyFix(@NotNull RefEntity[] refElements) {
final EntryPointsManager entryPointsManager = getEntryPointsManager();
for (RefEntity refElement : refElements) {
if (refElement instanceof RefElement) {
entryPointsManager.addEntryPoint((RefElement)refElement, true);
}
}
return true;
}
}
class CommentOutBin extends QuickFixAction {
CommentOutBin(@NotNull InspectionToolWrapper toolWrapper) {
super(COMMENT_OUT_QUICK_FIX, null, KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK),
toolWrapper);
}
@Override
protected boolean applyFix(@NotNull RefEntity[] refElements) {
if (!super.applyFix(refElements)) return false;
List<RefElement> deletedRefs = new ArrayList<RefElement>(1);
for (RefEntity refElement : refElements) {
PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getElement() : null;
if (psiElement == null) continue;
if (getFilter().getElementProblemCount((RefJavaElement)refElement) == 0) continue;
commentOutDead(psiElement);
refElement.getRefManager().removeRefElement((RefElement)refElement, deletedRefs);
}
EntryPointsManager entryPointsManager = getEntryPointsManager();
for (RefElement refElement : deletedRefs) {
entryPointsManager.removeEntryPoint(refElement);
}
return true;
}
}
private static final String COMMENT_OUT_QUICK_FIX = InspectionsBundle.message("inspection.dead.code.comment.quickfix");
private static class CommentOutFix implements IntentionAction {
private final PsiElement myElement;
private CommentOutFix(final PsiElement element) {
myElement = element;
}
@Override
@NotNull
public String getText() {
return COMMENT_OUT_QUICK_FIX;
}
@Override
@NotNull
public String getFamilyName() {
return getText();
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return true;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
if (myElement != null && myElement.isValid()) {
commentOutDead(PsiTreeUtil.getParentOfType(myElement, PsiModifierListOwner.class));
}
}
@Override
public boolean startInWriteAction() {
return true;
}
}
private static void commentOutDead(PsiElement psiElement) {
PsiFile psiFile = psiElement.getContainingFile();
if (psiFile != null) {
Document doc = PsiDocumentManager.getInstance(psiElement.getProject()).getDocument(psiFile);
if (doc != null) {
TextRange textRange = psiElement.getTextRange();
String date = DateFormatUtil.formatDateTime(new Date());
int startOffset = textRange.getStartOffset();
CharSequence chars = doc.getCharsSequence();
while (CharArrayUtil.regionMatches(chars, startOffset, InspectionsBundle.message("inspection.dead.code.comment"))) {
int line = doc.getLineNumber(startOffset) + 1;
if (line < doc.getLineCount()) {
startOffset = doc.getLineStartOffset(line);
startOffset = CharArrayUtil.shiftForward(chars, startOffset, " \t");
}
}
int endOffset = textRange.getEndOffset();
int line1 = doc.getLineNumber(startOffset);
int line2 = doc.getLineNumber(endOffset - 1);
if (line1 == line2) {
doc.insertString(startOffset, InspectionsBundle.message("inspection.dead.code.date.comment", date));
}
else {
for (int i = line1; i <= line2; i++) {
doc.insertString(doc.getLineStartOffset(i), "//");
}
doc.insertString(doc.getLineStartOffset(Math.min(line2 + 1, doc.getLineCount() - 1)),
InspectionsBundle.message("inspection.dead.code.stop.comment", date));
doc.insertString(doc.getLineStartOffset(line1), InspectionsBundle.message("inspection.dead.code.start.comment", date));
}
}
}
}
@NotNull
@Override
public InspectionNode createToolNode(@NotNull GlobalInspectionContextImpl context,
@NotNull InspectionNode node,
@NotNull InspectionRVContentProvider provider,
@NotNull InspectionTreeNode parentNode,
boolean showStructure) {
final EntryPointsNode entryPointsNode = new EntryPointsNode(context);
InspectionToolWrapper dummyToolWrapper = entryPointsNode.getToolWrapper();
InspectionToolPresentation presentation = context.getPresentation(dummyToolWrapper);
presentation.updateContent();
provider.appendToolNodeContent(context, entryPointsNode, node, showStructure);
return entryPointsNode;
}
@Override
public void updateContent() {
getTool().checkForReachables(getContext());
myPackageContents = new HashMap<String, Set<RefEntity>>();
getContext().getRefManager().iterate(new RefJavaVisitor() {
@Override public void visitElement(@NotNull RefEntity refEntity) {
if (!(refEntity instanceof RefJavaElement)) return;//dead code doesn't work with refModule | refPackage
RefJavaElement refElement = (RefJavaElement)refEntity;
if (!(getContext().getUIOptions().FILTER_RESOLVED_ITEMS && getIgnoredRefElements().contains(refElement)) && refElement.isValid() && getFilter().accepts(refElement)) {
String packageName = RefJavaUtil.getInstance().getPackageName(refEntity);
Set<RefEntity> content = myPackageContents.get(packageName);
if (content == null) {
content = new HashSet<RefEntity>();
myPackageContents.put(packageName, content);
}
content.add(refEntity);
}
}
});
}
@Override
public boolean hasReportedProblems() {
final GlobalInspectionContextImpl context = getContext();
if (!isDisposed() && context.getUIOptions().SHOW_ONLY_DIFF){
return containsOnlyDiff(myPackageContents) ||
myOldPackageContents != null && containsOnlyDiff(myOldPackageContents);
}
if (!myPackageContents.isEmpty()) return true;
return isOldProblemsIncluded() && !myOldPackageContents.isEmpty();
}
private boolean containsOnlyDiff(@NotNull Map<String, Set<RefEntity>> packageContents) {
for (String packageName : packageContents.keySet()) {
final Set<RefEntity> refElements = packageContents.get(packageName);
if (refElements != null){
for (RefEntity refElement : refElements) {
if (getElementStatus(refElement) != FileStatus.NOT_CHANGED){
return true;
}
}
}
}
return false;
}
@Override
public Map<String, Set<RefEntity>> getContent() {
return myPackageContents;
}
@Override
public Map<String, Set<RefEntity>> getOldContent() {
return myOldPackageContents;
}
@Override
public void ignoreCurrentElement(RefEntity refEntity) {
if (refEntity == null) return;
myIgnoreElements.add(refEntity);
}
@Override
public void amnesty(RefEntity refEntity) {
myIgnoreElements.remove(refEntity);
}
@Override
public void cleanup() {
super.cleanup();
myOldPackageContents = null;
myPackageContents.clear();
myIgnoreElements.clear();
}
@Override
public void finalCleanup() {
super.finalCleanup();
myOldPackageContents = null;
}
@Override
public boolean isGraphNeeded() {
return true;
}
@Override
public boolean isElementIgnored(final RefEntity element) {
for (RefEntity entity : myIgnoreElements) {
if (Comparing.equal(entity, element)) {
return true;
}
}
return false;
}
@NotNull
@Override
public FileStatus getElementStatus(final RefEntity element) {
final GlobalInspectionContextImpl context = getContext();
if (!isDisposed() && context.getUIOptions().SHOW_DIFF_WITH_PREVIOUS_RUN){
if (myOldPackageContents != null){
final boolean old = RefUtil.contains(element, collectRefElements(myOldPackageContents));
final boolean current = RefUtil.contains(element, collectRefElements(myPackageContents));
return calcStatus(old, current);
}
return FileStatus.ADDED;
}
return FileStatus.NOT_CHANGED;
}
@Override
@NotNull
public Collection<RefEntity> getIgnoredRefElements() {
return myIgnoreElements;
}
private static Set<RefEntity> collectRefElements(Map<String, Set<RefEntity>> packageContents) {
Set<RefEntity> allAvailable = new HashSet<RefEntity>();
for (Set<RefEntity> elements : packageContents.values()) {
allAvailable.addAll(elements);
}
return allAvailable;
}
@Override
@Nullable
public IntentionAction findQuickFixes(@NotNull final CommonProblemDescriptor descriptor, final String hint) {
if (descriptor instanceof ProblemDescriptor) {
if (DELETE.equals(hint)) {
return new PermanentDeleteFix(((ProblemDescriptor)descriptor).getPsiElement());
}
if (COMMENT.equals(hint)) {
return new CommentOutFix(((ProblemDescriptor)descriptor).getPsiElement());
}
}
return null;
}
private static class PermanentDeleteFix implements IntentionAction {
private final PsiElement myElement;
private PermanentDeleteFix(final PsiElement element) {
myElement = element;
}
@Override
@NotNull
public String getText() {
return DELETE_QUICK_FIX;
}
@Override
@NotNull
public String getFamilyName() {
return getText();
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return true;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
if (myElement != null && myElement.isValid()) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
SafeDeleteHandler
.invoke(myElement.getProject(), new PsiElement[]{PsiTreeUtil.getParentOfType(myElement, PsiModifierListOwner.class)}, false);
}
});
}
}
@Override
public boolean startInWriteAction() {
return true;
}
}
}