don't highlight groovy operator methods as unused
[idea/community.git] / plugins / groovy / src / org / jetbrains / plugins / groovy / codeInspection / local / GroovyPostHighlightingPass.java
1 /*
2  * Copyright 2000-2009 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
17 package org.jetbrains.plugins.groovy.codeInspection.local;
18
19 import com.intellij.codeHighlighting.TextEditorHighlightingPass;
20 import com.intellij.codeInsight.CodeInsightSettings;
21 import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
22 import com.intellij.codeInsight.daemon.HighlightDisplayKey;
23 import com.intellij.codeInsight.daemon.impl.*;
24 import com.intellij.codeInsight.intention.IntentionAction;
25 import com.intellij.codeInspection.InspectionProfile;
26 import com.intellij.codeInspection.ProblemHighlightType;
27 import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection;
28 import com.intellij.lang.annotation.Annotation;
29 import com.intellij.lang.annotation.AnnotationHolder;
30 import com.intellij.lang.annotation.AnnotationSession;
31 import com.intellij.lang.annotation.HighlightSeverity;
32 import com.intellij.openapi.application.ApplicationManager;
33 import com.intellij.openapi.command.CommandProcessor;
34 import com.intellij.openapi.editor.Editor;
35 import com.intellij.openapi.progress.ProgressIndicator;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.roots.ProjectFileIndex;
38 import com.intellij.openapi.roots.ProjectRootManager;
39 import com.intellij.openapi.util.TextRange;
40 import com.intellij.openapi.vfs.VirtualFile;
41 import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
42 import com.intellij.psi.*;
43 import com.intellij.util.Processor;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.plugins.groovy.codeInspection.GroovyInspectionBundle;
46 import org.jetbrains.plugins.groovy.codeInspection.GroovyUnusedDeclarationInspection;
47 import org.jetbrains.plugins.groovy.lang.completion.GroovyCompletionUtil;
48 import org.jetbrains.plugins.groovy.lang.editor.GroovyImportOptimizer;
49 import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
50 import org.jetbrains.plugins.groovy.lang.psi.GrNamedElement;
51 import org.jetbrains.plugins.groovy.lang.psi.GrReferenceElement;
52 import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
53 import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
54 import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult;
55 import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
56 import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
57 import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
58 import org.jetbrains.plugins.groovy.lang.psi.api.toplevel.imports.GrImportStatement;
59
60 import java.util.ArrayList;
61 import java.util.HashSet;
62 import java.util.List;
63 import java.util.Set;
64
65 /**
66  * @author ilyas
67  */
68 public class GroovyPostHighlightingPass extends TextEditorHighlightingPass {
69   private final GroovyFile myFile;
70   private final Editor myEditor;
71   private volatile Set<GrImportStatement> myUnusedImports;
72   private volatile Runnable myOptimizeRunnable;
73   private volatile List<HighlightInfo> myUnusedDeclarations;
74
75   public GroovyPostHighlightingPass(GroovyFile file, Editor editor) {
76     super(file.getProject(), editor.getDocument(), true);
77     myFile = file;
78     myEditor = editor;
79   }
80
81   public void doCollectInformation(final ProgressIndicator progress) {
82     final InspectionProfile profile = InspectionProjectProfileManager.getInstance(myProject).getInspectionProfile();
83     final boolean deadCodeEnabled = profile.isToolEnabled(HighlightDisplayKey.find(GroovyUnusedDeclarationInspection.SHORT_NAME), myFile);
84     ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
85     VirtualFile virtualFile = myFile.getViewProvider().getVirtualFile();
86     if (!fileIndex.isInContent(virtualFile)) {
87       return;
88     }
89     final UnusedDeclarationInspection deadCodeInspection = (UnusedDeclarationInspection)profile.getInspectionTool(UnusedDeclarationInspection.SHORT_NAME, myFile);
90     final GlobalUsageHelper usageHelper = new GlobalUsageHelper() {
91       public boolean isCurrentFileAlreadyChecked() {
92         return false;
93       }
94
95       public boolean isLocallyUsed(@NotNull PsiNamedElement member) {
96         return false;
97       }
98
99       @Override
100       public boolean shouldCheckUsages(@NotNull PsiMember member) {
101         return deadCodeInspection == null || !deadCodeInspection.isEntryPoint(member);
102       }
103     };
104
105     final List<HighlightInfo> unusedDeclarations = new ArrayList<HighlightInfo>();
106     final Set<GrImportStatement> unusedImports = new HashSet<GrImportStatement>(GroovyImportOptimizer.getValidImportStatements(myFile));
107     myFile.accept(new PsiRecursiveElementWalkingVisitor() {
108       @Override
109       public void visitElement(PsiElement element) {
110         if (element instanceof GrReferenceElement) {
111           for (GroovyResolveResult result : ((GrReferenceElement)element).multiResolve(true)) {
112             GroovyPsiElement context = result.getCurrentFileResolveContext();
113             if (context instanceof GrImportStatement) {
114               GrImportStatement importStatement = (GrImportStatement)context;
115               unusedImports.remove(importStatement);
116             }
117           }
118         }
119
120         if (deadCodeEnabled && element instanceof GrNamedElement && !PostHighlightingPass.isImplicitUsage((GrNamedElement)element, progress)) {
121           PsiElement nameId = ((GrNamedElement)element).getNameIdentifierGroovy();
122           if (nameId.getNode().getElementType() == GroovyTokenTypes.mIDENT) {
123             String name = ((GrNamedElement)element).getName();
124             if (element instanceof GrTypeDefinition && !PostHighlightingPass.isClassUsed((GrTypeDefinition)element, progress, usageHelper)) {
125               unusedDeclarations.add(
126                 PostHighlightingPass.createUnusedSymbolInfo(nameId, "Class " + name + " is unused", HighlightInfoType.UNUSED_SYMBOL));
127             }
128             else if (element instanceof GrMethod) {
129               GrMethod method = (GrMethod)element;
130               if (!GroovyCompletionUtil.OPERATOR_METHOD_NAMES.contains(method.getName()) &&
131                   !PostHighlightingPass.isMethodReferenced(method, progress, usageHelper)) {
132                 unusedDeclarations.add(
133                   PostHighlightingPass.createUnusedSymbolInfo(nameId, (method.isConstructor() ? "Constructor" : "Method") +" " + name + " is unused", HighlightInfoType.UNUSED_SYMBOL));
134               }
135             }
136             else if (element instanceof GrField && PostHighlightingPass.isFieldUnused((GrField)element, progress, usageHelper)) {
137               unusedDeclarations.add(
138                 PostHighlightingPass.createUnusedSymbolInfo(nameId, "Property " + name + " is unused", HighlightInfoType.UNUSED_SYMBOL));
139             }
140           }
141         }
142         
143         super.visitElement(element);
144       }
145     });
146     myUnusedImports = unusedImports;
147     myUnusedDeclarations = unusedDeclarations;
148     if (!unusedImports.isEmpty() && CodeInsightSettings.getInstance().OPTIMIZE_IMPORTS_ON_THE_FLY) {
149       final VirtualFile vfile = myFile.getVirtualFile();
150       if (vfile != null && ProjectRootManager.getInstance(myFile.getProject()).getFileIndex().isInSource(vfile)) {
151         final GrImportStatement[] imports = myFile.getImportStatements();
152         if (imports.length > 0) {
153           final int offset = myEditor.getCaretModel().getOffset();
154           if (imports[0].getTextRange().getStartOffset() <= offset && offset <= imports[imports.length - 1].getTextRange().getEndOffset()) {
155             return;
156           }
157         }
158
159         myOptimizeRunnable = new GroovyImportOptimizer().processFile(myFile);
160       }
161     }
162
163   }
164
165   private static IntentionAction createUnusedImportIntention() {
166     return new IntentionAction() {
167
168       @NotNull
169       public String getText() {
170         return GroovyInspectionBundle.message("optimize.all.imports");
171       }
172
173       @NotNull
174       public String getFamilyName() {
175         return GroovyInspectionBundle.message("optimize.imports");
176       }
177
178       public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
179         return true;
180       }
181
182       public void invoke(@NotNull final Project project, Editor editor, PsiFile file) {
183         optimizeImports(project, file);
184       }
185
186       public boolean startInWriteAction() {
187         return true;
188       }
189     };
190   }
191
192   public static void optimizeImports(final Project project, PsiFile file) {
193     GroovyImportOptimizer optimizer = new GroovyImportOptimizer();
194     final Runnable runnable = optimizer.processFile(file);
195     ApplicationManager.getApplication().runWriteAction(new Runnable() {
196       public void run() {
197         CommandProcessor.getInstance().executeCommand(project, runnable, "optimize imports", this);
198       }
199     });
200   }
201
202   public void doApplyInformationToEditor() {
203     if (myUnusedDeclarations == null || myUnusedImports == null) {
204       return;
205     }
206
207     AnnotationHolder annotationHolder = new AnnotationHolderImpl(new AnnotationSession(myFile));
208     List<HighlightInfo> infos = new ArrayList<HighlightInfo>(myUnusedDeclarations);
209     for (GrImportStatement unusedImport : myUnusedImports) {
210       Annotation annotation = annotationHolder.createWarningAnnotation(unusedImport, GroovyInspectionBundle.message("unused.import"));
211       annotation.setHighlightType(ProblemHighlightType.LIKE_UNUSED_SYMBOL);
212       annotation.registerFix(createUnusedImportIntention());
213       infos.add(HighlightInfo.fromAnnotation(annotation));
214     }
215
216     UpdateHighlightersUtil.setHighlightersToEditor(myProject, myDocument, 0, myFile.getTextLength(), infos, getColorsScheme(), getId());
217
218     final Runnable optimize = myOptimizeRunnable;
219     if (optimize != null && timeToOptimizeImports()) {
220       PostHighlightingPass.invokeOnTheFlyImportOptimizer(new Runnable() {
221         @Override
222         public void run() {
223           optimize.run();
224         }
225       }, myFile, myEditor);
226     }
227   }
228
229   private boolean timeToOptimizeImports() {
230     if (!CodeInsightSettings.getInstance().OPTIMIZE_IMPORTS_ON_THE_FLY) return false;
231
232     DaemonCodeAnalyzerImpl codeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(myProject);
233     if (!codeAnalyzer.isHighlightingAvailable(myFile)) return false;
234
235     if (!codeAnalyzer.isErrorAnalyzingFinished(myFile)) return false;
236     boolean errors = containsErrorsPreventingOptimize();
237
238     return !errors && codeAnalyzer.canChangeFileSilently(myFile);
239   }
240
241   private boolean containsErrorsPreventingOptimize() {
242     // ignore unresolved imports errors
243     final TextRange ignoreRange;
244     final GrImportStatement[] imports = myFile.getImportStatements();
245     if (imports.length != 0) {
246       final int start = imports[0].getTextRange().getStartOffset();
247       final int end = imports[imports.length - 1].getTextRange().getEndOffset();
248       ignoreRange = new TextRange(start, end);
249     } else {
250       ignoreRange = TextRange.EMPTY_RANGE;
251     }
252
253     return !DaemonCodeAnalyzerImpl.processHighlights(myDocument, myProject, HighlightSeverity.ERROR, 0, myDocument.getTextLength(), new Processor<HighlightInfo>() {
254       public boolean process(HighlightInfo error) {
255         int infoStart = error.getActualStartOffset();
256         int infoEnd = error.getActualEndOffset();
257
258         return ignoreRange.containsRange(infoStart,infoEnd) && error.type.equals(HighlightInfoType.WRONG_REF);
259       }
260     });
261   }
262
263
264 }