blob: 3a8fb591a605ba1b3aa08a545c7faf04f016339e [file] [log] [blame]
/*
* Copyright 2000-2014 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 org.jetbrains.plugins.groovy.codeInspection.local;
import com.intellij.codeHighlighting.TextEditorHighlightingPass;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInsight.daemon.impl.*;
import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil;
import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixAction;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.QuickFixFactory;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.AnnotationSession;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiClassImplUtil;
import com.intellij.psi.search.searches.OverridingMethodsSearch;
import com.intellij.psi.search.searches.SuperMethodsSearch;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.groovy.codeInspection.GroovyInspectionBundle;
import org.jetbrains.plugins.groovy.codeInspection.GroovyQuickFixFactory;
import org.jetbrains.plugins.groovy.codeInspection.GroovySuppressableInspectionTool;
import org.jetbrains.plugins.groovy.codeInspection.GroovyUnusedDeclarationInspection;
import org.jetbrains.plugins.groovy.lang.psi.util.GroovyImportUtil;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
import org.jetbrains.plugins.groovy.lang.psi.GrNamedElement;
import org.jetbrains.plugins.groovy.lang.psi.GrReferenceElement;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.toplevel.imports.GrImportStatement;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
import java.util.*;
/**
* @author ilyas
*/
public class GroovyPostHighlightingPass extends TextEditorHighlightingPass {
private final GroovyFile myFile;
private final Editor myEditor;
private volatile Set<GrImportStatement> myUnusedImports;
private volatile List<HighlightInfo> myUnusedDeclarations;
public GroovyPostHighlightingPass(GroovyFile file, Editor editor) {
super(file.getProject(), editor.getDocument(), true);
myFile = file;
myEditor = editor;
}
@Override
public void doCollectInformation(@NotNull final ProgressIndicator progress) {
ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
VirtualFile virtualFile = myFile.getViewProvider().getVirtualFile();
if (!fileIndex.isInContent(virtualFile)) {
return;
}
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(myProject).getInspectionProfile();
final HighlightDisplayKey unusedDefKey = HighlightDisplayKey.find(GroovyUnusedDeclarationInspection.SHORT_NAME);
final boolean deadCodeEnabled = profile.isToolEnabled(unusedDefKey, myFile);
final UnusedDeclarationInspection deadCodeInspection = (UnusedDeclarationInspection)profile.getUnwrappedTool(UnusedDeclarationInspection.SHORT_NAME, myFile);
final GlobalUsageHelper usageHelper = new GlobalUsageHelper() {
@Override
public boolean isCurrentFileAlreadyChecked() {
return false;
}
@Override
public boolean isLocallyUsed(@NotNull PsiNamedElement member) {
return false;
}
@Override
public boolean shouldCheckUsages(@NotNull PsiMember member) {
return deadCodeInspection == null || !deadCodeInspection.isEntryPoint(member);
}
};
final List<HighlightInfo> unusedDeclarations = new ArrayList<HighlightInfo>();
final Map<GrParameter, Boolean> usedParams = new HashMap<GrParameter, Boolean>();
myFile.accept(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
if (element instanceof GrReferenceElement) {
for (GroovyResolveResult result : ((GrReferenceElement)element).multiResolve(true)) {
PsiElement resolved = result.getElement();
if (resolved instanceof GrParameter && resolved.getContainingFile() == myFile) {
usedParams.put((GrParameter)resolved, Boolean.TRUE);
}
}
}
if (deadCodeEnabled &&
element instanceof GrNamedElement && element instanceof PsiModifierListOwner &&
!PostHighlightingPass.isImplicitUsage(element.getProject(), (PsiModifierListOwner)element, progress) &&
!GroovySuppressableInspectionTool.isElementToolSuppressedIn(element, GroovyUnusedDeclarationInspection.SHORT_NAME)) {
PsiElement nameId = ((GrNamedElement)element).getNameIdentifierGroovy();
if (nameId.getNode().getElementType() == GroovyTokenTypes.mIDENT) {
String name = ((GrNamedElement)element).getName();
if (element instanceof GrTypeDefinition && !PostHighlightingPass.isClassUsed(myProject,
element.getContainingFile(), (GrTypeDefinition)element, progress, usageHelper
)) {
HighlightInfo highlightInfo = PostHighlightingPass.createUnusedSymbolInfo(nameId, "Class " + name + " is unused", HighlightInfoType.UNUSED_SYMBOL);
QuickFixAction.registerQuickFixAction(highlightInfo, QuickFixFactory.getInstance().createSafeDeleteFix(element), unusedDefKey);
ContainerUtil.addIfNotNull(unusedDeclarations, highlightInfo);
}
else if (element instanceof GrMethod) {
GrMethod method = (GrMethod)element;
if (!PostHighlightingPass.isMethodReferenced(method.getProject(), method.getContainingFile(), method, progress, usageHelper)) {
String message = (method.isConstructor() ? "Constructor" : "Method") + " " + name + " is unused";
HighlightInfo highlightInfo = PostHighlightingPass.createUnusedSymbolInfo(nameId, message, HighlightInfoType.UNUSED_SYMBOL);
QuickFixAction.registerQuickFixAction(highlightInfo, QuickFixFactory.getInstance().createSafeDeleteFix(method), unusedDefKey);
ContainerUtil.addIfNotNull(unusedDeclarations, highlightInfo);
}
}
else if (element instanceof GrField && isFieldUnused((GrField)element, progress, usageHelper)) {
HighlightInfo highlightInfo =
PostHighlightingPass.createUnusedSymbolInfo(nameId, "Property " + name + " is unused", HighlightInfoType.UNUSED_SYMBOL);
QuickFixAction.registerQuickFixAction(highlightInfo, QuickFixFactory.getInstance().createSafeDeleteFix(element), unusedDefKey);
ContainerUtil.addIfNotNull(unusedDeclarations, highlightInfo);
}
else if (element instanceof GrParameter) {
if (!usedParams.containsKey(element)) {
usedParams.put((GrParameter)element, Boolean.FALSE);
}
}
}
}
super.visitElement(element);
}
});
final Set<GrImportStatement> unusedImports = new HashSet<GrImportStatement>(PsiUtil.getValidImportStatements(myFile));
unusedImports.removeAll(GroovyImportUtil.findUsedImports(myFile));
myUnusedImports = unusedImports;
if (deadCodeEnabled) {
for (GrParameter parameter : usedParams.keySet()) {
if (usedParams.get(parameter)) continue;
PsiElement scope = parameter.getDeclarationScope();
if (scope instanceof GrMethod) {
GrMethod method = (GrMethod)scope;
if (methodMayHaveUnusedParameters(method)) {
PsiElement identifier = parameter.getNameIdentifierGroovy();
HighlightInfo highlightInfo = PostHighlightingPass
.createUnusedSymbolInfo(identifier, "Parameter " + parameter.getName() + " is unused", HighlightInfoType.UNUSED_SYMBOL);
QuickFixAction.registerQuickFixAction(highlightInfo, GroovyQuickFixFactory.getInstance().createRemoveUnusedGrParameterFix(parameter), unusedDefKey);
ContainerUtil.addIfNotNull(unusedDeclarations, highlightInfo);
}
}
else if (scope instanceof GrClosableBlock) {
//todo Max Medvedev
}
}
}
myUnusedDeclarations = unusedDeclarations;
}
private static boolean methodMayHaveUnusedParameters(GrMethod method) {
return (method.isConstructor() ||
method.hasModifierProperty(PsiModifier.PRIVATE) ||
method.hasModifierProperty(PsiModifier.STATIC) ||
!method.hasModifierProperty(PsiModifier.ABSTRACT) && !isOverriddenOrOverrides(method)) &&
!method.hasModifierProperty(PsiModifier.NATIVE) &&
!JavaHighlightUtil.isSerializationRelatedMethod(method, method.getContainingClass()) &&
!PsiClassImplUtil.isMainOrPremainMethod(method);
}
private static boolean isFieldUnused(GrField field, ProgressIndicator progress, GlobalUsageHelper usageHelper) {
if (!PostHighlightingPass.isFieldUnused(field.getProject(), field.getContainingFile(), field, progress, usageHelper)) return false;
final GrAccessorMethod[] getters = field.getGetters();
final GrAccessorMethod setter = field.getSetter();
for (GrAccessorMethod getter : getters) {
if (getter.findSuperMethods().length > 0) {
return false;
}
}
if (setter != null) {
if (setter.findSuperMethods().length > 0) {
return false;
}
}
return true;
}
private static boolean isOverriddenOrOverrides(PsiMethod method) {
boolean overrides = SuperMethodsSearch.search(method, null, true, false).findFirst() != null;
return overrides || OverridingMethodsSearch.search(method).findFirst() != null;
}
@Override
public void doApplyInformationToEditor() {
if (myUnusedDeclarations == null || myUnusedImports == null) {
return;
}
AnnotationHolder annotationHolder = new AnnotationHolderImpl(new AnnotationSession(myFile));
List<HighlightInfo> infos = new ArrayList<HighlightInfo>(myUnusedDeclarations);
for (GrImportStatement unusedImport : myUnusedImports) {
Annotation annotation = annotationHolder.createWarningAnnotation(calculateRangeToUse(unusedImport), GroovyInspectionBundle.message("unused.import"));
annotation.setHighlightType(ProblemHighlightType.LIKE_UNUSED_SYMBOL);
annotation.registerFix(GroovyQuickFixFactory.getInstance().createOptimizeImportsFix(false));
infos.add(HighlightInfo.fromAnnotation(annotation));
}
UpdateHighlightersUtil.setHighlightersToEditor(myProject, myDocument, 0, myFile.getTextLength(), infos, getColorsScheme(), getId());
if (myUnusedImports != null && !myUnusedImports.isEmpty()) {
IntentionAction fix = GroovyQuickFixFactory.getInstance().createOptimizeImportsFix(true);
if (fix.isAvailable(myProject, myEditor, myFile) && myFile.isWritable()) {
fix.invoke(myProject, myEditor, myFile);
}
}
}
private static TextRange calculateRangeToUse(GrImportStatement unusedImport) {
final TextRange range = unusedImport.getTextRange();
if (StringUtil.isEmptyOrSpaces(unusedImport.getAnnotationList().getText())) return range;
int start = 0;
for (PsiElement child = unusedImport.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getNode().getElementType() == GroovyTokenTypes.kIMPORT) {
start = child.getTextRange().getStartOffset();
}
}
return new TextRange(start, range.getEndOffset());
}
}