blob: df49b83ff48d400813cbf0aae28184d43a9fd4e2 [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 com.intellij.codeInspection.inferNullity;
import com.intellij.analysis.AnalysisScope;
import com.intellij.analysis.BaseAnalysisAction;
import com.intellij.analysis.BaseAnalysisActionDialog;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.daemon.impl.quickfix.LocateLibraryDialog;
import com.intellij.codeInsight.daemon.impl.quickfix.OrderEntryFix;
import com.intellij.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryUtil;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.VerticalFlowLayout;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Factory;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.ui.TitledSeparator;
import com.intellij.usageView.UsageInfo;
import com.intellij.usageView.UsageViewUtil;
import com.intellij.usages.*;
import com.intellij.util.Function;
import com.intellij.util.Processor;
import com.intellij.util.SequentialModalProgressTask;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class InferNullityAnnotationsAction extends BaseAnalysisAction {
@NonNls private static final String INFER_NULLITY_ANNOTATIONS = "Infer Nullity Annotations";
@NonNls private static final String ANNOTATE_LOCAL_VARIABLES = "annotate.local.variables";
private JCheckBox myAnnotateLocalVariablesCb;
public InferNullityAnnotationsAction() {
super("Infer Nullity", INFER_NULLITY_ANNOTATIONS);
}
@Override
protected void analyze(@NotNull final Project project, @NotNull final AnalysisScope scope) {
PropertiesComponent.getInstance().setValue(ANNOTATE_LOCAL_VARIABLES, String.valueOf(myAnnotateLocalVariablesCb.isSelected()));
final ProgressManager progressManager = ProgressManager.getInstance();
final int totalFiles = scope.getFileCount();
final Set<Module> modulesWithoutAnnotations = new HashSet<Module>();
final Set<Module> modulesWithLL = new HashSet<Module>();
if (!progressManager.runProcessWithProgressSynchronously(new Runnable() {
@Override
public void run() {
scope.accept(new PsiElementVisitor() {
private int myFileCount = 0;
final private Set<Module> processed = new HashSet<Module>();
@Override
public void visitFile(PsiFile file) {
myFileCount++;
final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
if (progressIndicator != null) {
final VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile != null) {
progressIndicator.setText2(ProjectUtil.calcRelativeToProjectPath(virtualFile, project));
}
progressIndicator.setFraction(((double)myFileCount) / totalFiles);
}
final Module module = ModuleUtilCore.findModuleForPsiElement(file);
if (module != null && !processed.contains(module)) {
processed.add(module);
if (JavaPsiFacade.getInstance(project)
.findClass(NullableNotNullManager.getInstance(project).getDefaultNullable(),
GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module)) == null) {
modulesWithoutAnnotations.add(module);
}
if (PsiUtil.getLanguageLevel(file).compareTo(LanguageLevel.JDK_1_5) < 0) {
modulesWithLL.add(module);
}
}
}
});
}
}, "Check applicability...", true, project)) {
return;
}
if (!modulesWithLL.isEmpty()) {
Messages.showErrorDialog(project, "Infer Nullity Annotations requires the project language level be set to 1.5 or greater.",
INFER_NULLITY_ANNOTATIONS);
return;
}
if (!modulesWithoutAnnotations.isEmpty()) {
final Library annotationsLib =
LibraryUtil.findLibraryByClass(NullableNotNullManager.getInstance(project).getDefaultNullable(), project);
if (annotationsLib != null) {
String message = "Module" + (modulesWithoutAnnotations.size() == 1 ? " " : "s ");
message += StringUtil.join(modulesWithoutAnnotations, new Function<Module, String>() {
@Override
public String fun(Module module) {
return module.getName();
}
}, ", ");
message += (modulesWithoutAnnotations.size() == 1 ? " doesn't" : " don't");
message += " refer to the existing '" +
annotationsLib.getName() +
"' library with IDEA nullity annotations. Would you like to add the dependenc";
message += (modulesWithoutAnnotations.size() == 1 ? "y" : "ies") + " now?";
if (Messages.showOkCancelDialog(project, message, INFER_NULLITY_ANNOTATIONS, Messages.getErrorIcon()) ==
Messages.OK) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
for (Module module : modulesWithoutAnnotations) {
ModuleRootModificationUtil.addDependency(module, annotationsLib);
}
}
});
restartAnalysis(project, scope);
}
}
else if (Messages.showOkCancelDialog(project, "Infer Nullity Annotations requires that the nullity annotations" +
" be available in all your project sources.\n\nYou will need to add annotations.jar as a library. " +
"It is possible to configure custom JAR in e.g. Constant Conditions & Exceptions inspection or use JetBrains annotations available in installation. " +
" IntelliJ IDEA nullity annotations are freely usable and redistributable under the Apache 2.0 license. Would you like to do it now?",
INFER_NULLITY_ANNOTATIONS, Messages.getErrorIcon()) == Messages.OK) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
final LocateLibraryDialog dialog =
new LocateLibraryDialog(modulesWithoutAnnotations.iterator().next(), PathManager.getLibPath(), "annotations.jar",
QuickFixBundle.message("add.library.annotations.description"));
dialog.show();
if (dialog.isOK()) {
final String path = dialog.getResultingLibraryPath();
new WriteCommandAction(project) {
@Override
protected void run(@NotNull final Result result) throws Throwable {
for (Module module : modulesWithoutAnnotations) {
OrderEntryFix.addBundledJarToRoots(project, null, module, null, AnnotationUtil.NOT_NULL, path);
}
}
}.execute();
restartAnalysis(project, scope);
}
}
});
}
return;
}
if (scope.checkScopeWritable(project)) return;
PsiDocumentManager.getInstance(project).commitAllDocuments();
final UsageInfo[] usageInfos = findUsages(project, scope);
if (usageInfos == null) return;
if (usageInfos.length < 5) {
SwingUtilities.invokeLater(applyRunnable(project, new Computable<UsageInfo[]>() {
@Override
public UsageInfo[] compute() {
return usageInfos;
}
}));
}
else {
showUsageView(project, usageInfos, scope);
}
}
private UsageInfo[] findUsages(@NotNull final Project project,
@NotNull final AnalysisScope scope) {
final NullityInferrer inferrer = new NullityInferrer(myAnnotateLocalVariablesCb.isSelected(), project);
final PsiManager psiManager = PsiManager.getInstance(project);
final Runnable searchForUsages = new Runnable() {
@Override
public void run() {
final int totalFiles = scope.getFileCount();
scope.accept(new PsiElementVisitor() {
int myFileCount = 0;
@Override
public void visitFile(final PsiFile file) {
myFileCount++;
final VirtualFile virtualFile = file.getVirtualFile();
final FileViewProvider viewProvider = psiManager.findViewProvider(virtualFile);
final Document document = viewProvider == null ? null : viewProvider.getDocument();
if (document == null || virtualFile.getFileType().isBinary()) return; //do not inspect binary files
final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
if (progressIndicator != null) {
progressIndicator.setText2(ProjectUtil.calcRelativeToProjectPath(virtualFile, project));
progressIndicator.setFraction(((double)myFileCount) / totalFiles);
}
if (file instanceof PsiJavaFile) {
inferrer.collect(file);
}
}
});
}
};
if (ApplicationManager.getApplication().isDispatchThread()) {
if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(searchForUsages, INFER_NULLITY_ANNOTATIONS, true, project)) {
return null;
}
} else {
searchForUsages.run();
}
final List<UsageInfo> usages = new ArrayList<UsageInfo>();
inferrer.collect(usages);
return usages.toArray(new UsageInfo[usages.size()]);
}
private static Runnable applyRunnable(final Project project, final Computable<UsageInfo[]> computable) {
return new Runnable() {
@Override
public void run() {
final LocalHistoryAction action = LocalHistory.getInstance().startAction(INFER_NULLITY_ANNOTATIONS);
try {
new WriteCommandAction(project, INFER_NULLITY_ANNOTATIONS) {
@Override
protected void run(@NotNull Result result) throws Throwable {
final UsageInfo[] infos = computable.compute();
if (infos.length > 0) {
final SequentialModalProgressTask progressTask = new SequentialModalProgressTask(project, INFER_NULLITY_ANNOTATIONS, false);
progressTask.setMinIterationTime(200);
progressTask.setTask(new AnnotateTask(project, progressTask, infos));
ProgressManager.getInstance().run(progressTask);
} else {
NullityInferrer.nothingFoundMessage(project);
}
}
}.execute();
}
finally {
action.finish();
}
}
};
}
private void restartAnalysis(final Project project, final AnalysisScope scope) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
analyze(project, scope);
}
});
}
private void showUsageView(@NotNull Project project, final UsageInfo[] usageInfos, @NotNull AnalysisScope scope) {
final UsageTarget[] targets = UsageTarget.EMPTY_ARRAY;
final Ref<Usage[]> convertUsagesRef = new Ref<Usage[]>();
if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
convertUsagesRef.set(UsageInfo2UsageAdapter.convert(usageInfos));
}
});
}
}, "Preprocess usages", true, project)) return;
if (convertUsagesRef.isNull()) return;
final Usage[] usages = convertUsagesRef.get();
final UsageViewPresentation presentation = new UsageViewPresentation();
presentation.setTabText("Infer Nullity Preview");
presentation.setShowReadOnlyStatusAsRed(true);
presentation.setShowCancelButton(true);
presentation.setUsagesString(RefactoringBundle.message("usageView.usagesText"));
final UsageView usageView = UsageViewManager.getInstance(project).showUsages(targets, usages, presentation, rerunFactory(project, scope));
final Runnable refactoringRunnable = applyRunnable(project, new Computable<UsageInfo[]>() {
@Override
public UsageInfo[] compute() {
final Set<UsageInfo> infos = UsageViewUtil.getNotExcludedUsageInfos(usageView);
return infos.toArray(new UsageInfo[infos.size()]);
}
});
String canNotMakeString = "Cannot perform operation.\nThere were changes in code after usages have been found.\nPlease perform operation search again.";
usageView.addPerformOperationAction(refactoringRunnable, INFER_NULLITY_ANNOTATIONS, canNotMakeString, INFER_NULLITY_ANNOTATIONS, false);
}
@NotNull
private Factory<UsageSearcher> rerunFactory(@NotNull final Project project, @NotNull final AnalysisScope scope) {
return new Factory<UsageSearcher>() {
@Override
public UsageSearcher create() {
return new UsageInfoSearcherAdapter() {
@Override
protected UsageInfo[] findUsages() {
return InferNullityAnnotationsAction.this.findUsages(project, scope);
}
@Override
public void generate(@NotNull Processor<Usage> processor) {
processUsages(processor, project);
}
};
}
};
}
@Override
protected JComponent getAdditionalActionSettings(Project project, BaseAnalysisActionDialog dialog) {
final JPanel panel = new JPanel(new VerticalFlowLayout());
panel.add(new TitledSeparator());
myAnnotateLocalVariablesCb = new JCheckBox("Annotate local variables", PropertiesComponent.getInstance().getBoolean(ANNOTATE_LOCAL_VARIABLES, false));
panel.add(myAnnotateLocalVariablesCb);
return panel;
}
@Override
protected void canceled() {
super.canceled();
myAnnotateLocalVariablesCb = null;
}
}