blob: 8a2c70738ed34247c96c170ea2c1df6e3cc7b896 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.tools.idea.actions;
import com.android.tools.idea.gradle.parser.BuildFileKey;
import com.android.tools.idea.gradle.parser.BuildFileStatement;
import com.android.tools.idea.gradle.parser.Dependency;
import com.android.tools.idea.gradle.parser.GradleBuildFile;
import com.android.tools.idea.gradle.project.GradleProjectImporter;
import com.android.tools.idea.gradle.util.Projects;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.templates.RepositoryUrlManager;
import com.intellij.analysis.AnalysisScope;
import com.intellij.analysis.AnalysisScopeBundle;
import com.intellij.analysis.BaseAnalysisActionDialog;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInspection.inferNullity.InferNullityAnnotationsAction;
import com.intellij.codeInspection.inferNullity.NullityInferrer;
import com.intellij.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
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.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.ui.Messages;
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.psi.*;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.usageView.UsageInfo;
import com.intellij.usageView.UsageViewUtil;
import com.intellij.usages.*;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.Processor;
import com.intellij.util.SequentialModalProgressTask;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.util.*;
import static com.intellij.openapi.util.text.StringUtil.pluralize;
/**
* AndroidInferNullityAnnotationAction gives the user the option of adding the correct
* component library to the gradle build file.
* This file has excerpts of Intellij code.
*/
public class AndroidInferNullityAnnotationAction extends InferNullityAnnotationsAction {
private static final Logger LOG = Logger.getInstance(AndroidInferNullityAnnotationAction.class);
private static final String INFER_NULLITY_ANNOTATIONS = "Infer Nullity Annotations";
private static final String ADD_DEPENDENCY = "Add Support Dependency";
private static final int MIN_SDK_WITH_NULLABLE = 19;
@Override
protected void analyze(@NotNull Project project, @NotNull AnalysisScope scope) {
if (!Projects.isBuildWithGradle(project)) {
super.analyze(project, scope);
return;
}
int[] fileCount = new int[] {0};
Map<Module, PsiFile> modules = findModulesInScope(project, scope, fileCount);
if (modules == null) {
return;
}
if (!checkModules(project, scope, modules)) {
return;
}
PsiDocumentManager.getInstance(project).commitAllDocuments();
final UsageInfo[] usageInfos = findUsages(project, scope, fileCount[0]);
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);
}
}
// Intellij code from InferNullityAnnotationsAction.
private static Map<Module, PsiFile> findModulesInScope(@NotNull final Project project,
@NotNull final AnalysisScope scope,
@NotNull final int[] fileCount) {
final ProgressManager progressManager = ProgressManager.getInstance();
final Map<Module, PsiFile> modules = new HashMap<Module, PsiFile>();
boolean completed = progressManager.runProcessWithProgressSynchronously(new Runnable() {
@Override
public void run() {
scope.accept(new PsiElementVisitor() {
@Override
public void visitFile(PsiFile file) {
fileCount[0]++;
final ProgressIndicator progressIndicator = progressManager.getProgressIndicator();
if (progressIndicator != null) {
final VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile != null) {
progressIndicator.setText2(ProjectUtil.calcRelativeToProjectPath(virtualFile, project));
}
progressIndicator.setText(AnalysisScopeBundle.message("scanning.scope.progress.title"));
}
final Module module = ModuleUtilCore.findModuleForPsiElement(file);
if (module != null && !modules.containsKey(module)) {
modules.put(module, file);
}
}
});
}
}, "Check applicability...", true, project);
return completed ? modules : null;
}
// Intellij code from InferNullityAnnotationsAction.
private static UsageInfo[] findUsages(@NotNull final Project project,
@NotNull final AnalysisScope scope,
final int fileCount) {
final NullityInferrer inferrer = new NullityInferrer(false, project);
final PsiManager psiManager = PsiManager.getInstance(project);
final Runnable searchForUsages = new Runnable() {
@Override
public void run() {
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) / fileCount);
}
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()]);
}
// For Android we need to check SDK version and possibly update the gradle project file
protected boolean checkModules(@NotNull final Project project,
@NotNull final AnalysisScope scope,
@NotNull Map<Module, PsiFile> modules) {
RepositoryUrlManager manager = RepositoryUrlManager.get();
final String libraryCoordinate = manager.getLibraryCoordinate(RepositoryUrlManager.SUPPORT_ANNOTATIONS);
final Set<Module> modulesWithoutAnnotations = new HashSet<Module>();
final Set<Module> modulesWithLowVersion = new HashSet<Module>();
for (Module module : modules.keySet()) {
AndroidModuleInfo info = AndroidModuleInfo.get(module);
if (info != null && info.getBuildSdkVersion() != null && info.getBuildSdkVersion().getFeatureLevel() < MIN_SDK_WITH_NULLABLE) {
modulesWithLowVersion.add(module);
}
GradleBuildFile gradleBuildFile = GradleBuildFile.get(module);
if (gradleBuildFile == null) {
LOG.warn("Unable to find Gradle build file for module " + module.getModuleFilePath());
continue;
}
boolean dependencyFound = false;
for (BuildFileStatement entry : gradleBuildFile.getDependencies()) {
if (entry instanceof Dependency) {
Dependency dependency = (Dependency)entry;
if (dependency.scope == Dependency.Scope.COMPILE &&
dependency.type == Dependency.Type.EXTERNAL &&
dependency.getValueAsString().equals(libraryCoordinate)) {
dependencyFound = true;
break;
}
}
}
if (!dependencyFound) {
modulesWithoutAnnotations.add(module);
}
}
if (!modulesWithLowVersion.isEmpty()) {
Messages.showErrorDialog(
project,
String.format("Infer Nullity Annotations requires the project sdk level be set to %1$d or greater.", MIN_SDK_WITH_NULLABLE),
"Infer Nullity Annotations");
return false;
}
if (modulesWithoutAnnotations.isEmpty()) {
return true;
}
String moduleNames = StringUtil.join(modulesWithoutAnnotations, new Function<Module, String>() {
@Override
public String fun(Module module) {
return module.getName();
}
}, ", ");
int count = modulesWithoutAnnotations.size();
String message = String.format("The %1$s %2$s %3$sn't refer to the existing '%4$s' library with Android nullity annotations. \n\n" +
"Would you like to add the %5$s now?",
pluralize("module", count),
moduleNames,
count > 1 ? "do" : "does",
RepositoryUrlManager.SUPPORT_ANNOTATIONS,
pluralize("dependency", count));
if (Messages.showOkCancelDialog(project, message, "Infer Nullity Annotations", Messages.getErrorIcon()) == Messages.OK) {
final LocalHistoryAction action = LocalHistory.getInstance().startAction(ADD_DEPENDENCY);
try {
new WriteCommandAction(project, ADD_DEPENDENCY) {
@Override
protected void run(@NotNull final Result result) throws Throwable {
for (Module module : modulesWithoutAnnotations) {
addDependency(module, libraryCoordinate);
}
GradleProjectImporter.getInstance().requestProjectSync(project, false /* do not generate sources */, null);
}
}.execute();
restartAnalysis(project, scope);
}
finally {
action.finish();
}
}
return true;
}
// Intellij code from InferNullityAnnotationsAction.
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 Set<PsiElement> elements = new LinkedHashSet<PsiElement>();
for (UsageInfo info : infos) {
final PsiElement element = info.getElement();
if (element != null) {
ContainerUtil.addIfNotNull(elements, element.getContainingFile());
}
}
if (!FileModificationService.getInstance().preparePsiElementsForWrite(elements)) return;
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();
}
}
};
}
// Intellij code from InferNullityAnnotationsAction.
protected void restartAnalysis(final Project project, final AnalysisScope scope) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
analyze(project, scope);
}
});
}
// Intellij code from InferNullityAnnotationsAction.
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);
}
// Intellij code from InferNullityAnnotationsAction.
@NotNull
private static 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 AndroidInferNullityAnnotationAction.findUsages(project, scope, scope.getFileCount());
}
@Override
public void generate(@NotNull Processor<Usage> processor) {
processUsages(processor, project);
}
};
}
};
}
private static void addDependency(final Module module, final String libraryCoordinate) {
ModuleRootModificationUtil.updateModel(module, new Consumer<ModifiableRootModel>() {
@Override
public void consume(ModifiableRootModel model) {
GradleBuildFile gradleBuildFile = GradleBuildFile.get(module);
if (gradleBuildFile != null) {
List<BuildFileStatement> dependencies = gradleBuildFile.getDependencies();
dependencies.add(new Dependency(Dependency.Scope.COMPILE, Dependency.Type.EXTERNAL, libraryCoordinate));
gradleBuildFile.setValue(BuildFileKey.DEPENDENCIES, dependencies);
}
}
});
}
/* Android nullable annotations do not support annotations on local variables. */
@Override
protected JComponent getAdditionalActionSettings(Project project, BaseAnalysisActionDialog dialog) {
if (!Projects.isBuildWithGradle(project)) {
return super.getAdditionalActionSettings(project, dialog);
}
return null;
}
}