com.android.tools.idea.actions.annotations.InferSupportAnnotationsAction.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.actions.annotations.InferSupportAnnotationsAction.java

Source

/*
 * Copyright (C) 2016 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.annotations;

import com.android.SdkConstants;
import com.android.tools.idea.gradle.dsl.model.GradleBuildModel;
import com.android.tools.idea.gradle.dsl.model.dependencies.ArtifactDependencyModel;
import com.android.tools.idea.gradle.dsl.model.dependencies.DependenciesModel;
import com.android.tools.idea.gradle.project.GradleProjectInfo;
import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.templates.RepositoryUrlManager;
import com.android.tools.idea.templates.SupportLibrary;
import com.intellij.analysis.AnalysisScope;
import com.intellij.analysis.BaseAnalysisAction;
import com.intellij.analysis.BaseAnalysisActionDialog;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.ide.scratch.ScratchFileService;
import com.intellij.ide.scratch.ScratchRootType;
import com.intellij.lang.StdLanguages;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
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.fileEditor.FileEditorManager;
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.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.ObjectUtils;
import com.intellij.util.Processor;
import com.intellij.util.SequentialModalProgressTask;
import com.intellij.util.SequentialTask;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.*;

import static com.android.tools.idea.gradle.dsl.model.dependencies.CommonConfigurationNames.COMPILE;
import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
import static com.intellij.openapi.util.text.StringUtil.pluralize;

/**
 * Analyze support annotations
 */
public class InferSupportAnnotationsAction extends BaseAnalysisAction {
    /**
     * Whether this feature is enabled or not during development
     */
    static final boolean ENABLED = Boolean.valueOf(System.getProperty("studio.infer.annotations"));

    /**
     * Number of times we pass through the project files
     */
    static final int MAX_PASSES = 3;

    @NonNls
    private static final String INFER_SUPPORT_ANNOTATIONS = "Infer Support Annotations";
    private static final int MAX_ANNOTATIONS_WITHOUT_PREVIEW = 5;

    public InferSupportAnnotationsAction() {
        super("Infer Support Annotations", INFER_SUPPORT_ANNOTATIONS);
        if (!ENABLED) {
            getTemplatePresentation().setVisible(false);
        }
    }

    private static final String ADD_DEPENDENCY = "Add Support Dependency";
    private static final int MIN_SDK_WITH_NULLABLE = 19;

    @Override
    public void update(AnActionEvent event) {
        if (!ENABLED) {
            return;
        }
        super.update(event);
        Project project = event.getProject();
        if (project == null || !GradleProjectInfo.getInstance(project).isBuildWithGradle()) {
            Presentation presentation = event.getPresentation();
            presentation.setEnabled(false);
        }
    }

    @Override
    protected void analyze(@NotNull Project project, @NotNull AnalysisScope scope) {
        if (!GradleProjectInfo.getInstance(project).isBuildWithGradle()) {
            return;
        }
        int[] fileCount = new int[] { 0 };
        PsiDocumentManager.getInstance(project).commitAllDocuments();
        UsageInfo[] usageInfos = findUsages(project, scope, fileCount[0]);
        if (usageInfos == null)
            return;

        Map<Module, PsiFile> modules = findModulesFromUsage(usageInfos);

        if (!checkModules(project, scope, modules)) {
            return;
        }

        if (usageInfos.length < MAX_ANNOTATIONS_WITHOUT_PREVIEW) {
            ApplicationManager.getApplication().invokeLater(applyRunnable(project, () -> usageInfos));
        } else {
            showUsageView(project, usageInfos, scope);
        }
    }

    private static Map<Module, PsiFile> findModulesFromUsage(UsageInfo[] infos) {
        // We need 1 file from each module that requires changes (the file may be overwritten below):
        Map<Module, PsiFile> modules = new HashMap<>();

        for (UsageInfo info : infos) {
            PsiElement element = info.getElement();
            assert element != null;
            Module module = ModuleUtilCore.findModuleForPsiElement(element);
            if (module == null) {
                continue;
            }
            PsiFile file = element.getContainingFile();
            modules.put(module, file);
        }
        return modules;
    }

    private static UsageInfo[] findUsages(@NotNull Project project, @NotNull AnalysisScope scope, int fileCount) {
        InferSupportAnnotations inferrer = new InferSupportAnnotations(false, project);
        PsiManager psiManager = PsiManager.getInstance(project);
        Runnable searchForUsages = () -> scope.accept(new PsiElementVisitor() {
            int myFileCount = 0;

            @Override
            public void visitFile(PsiFile file) {
                myFileCount++;
                VirtualFile virtualFile = file.getVirtualFile();
                FileViewProvider viewProvider = psiManager.findViewProvider(virtualFile);
                Document document = viewProvider == null ? null : viewProvider.getDocument();
                if (document == null || virtualFile.getFileType().isBinary())
                    return; //do not inspect binary files
                ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
                if (progressIndicator != null) {
                    progressIndicator.setText2(ProjectUtil.calcRelativeToProjectPath(virtualFile, project));
                    progressIndicator.setFraction(((double) myFileCount) / (MAX_PASSES * fileCount));
                }
                if (file instanceof PsiJavaFile) {
                    inferrer.collect(file);
                }
            }
        });

        /*
          Collect these files and visit repeatedly. Consider this
          scenario, where I visit files A, B, C in alphabetical order.
          Let's say a method in A unconditionally calls a method in B
          calls a method in C. In file C I discover that the method
          requires permission P. At this point it's too late for me to
          therefore conclude that the method in B also requires it. If I
          make a whole separate pass again, I could now add that
          constraint. But only after that second pass can I infer that
          the method in A also requires it. In short, I need to keep
          passing through all files until I make no more progress. It
          would be much more efficient to handle this with a global call
          graph such that as soon as I make an inference I can flow it
          backwards.
         */
        Runnable multipass = () -> {
            for (int i = 0; i < MAX_PASSES; i++) {
                searchForUsages.run();
            }
        };

        if (ApplicationManager.getApplication().isDispatchThread()) {
            if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(multipass,
                    INFER_SUPPORT_ANNOTATIONS, true, project)) {
                return null;
            }
        } else {
            multipass.run();
        }

        List<UsageInfo> usages = new ArrayList<>();
        inferrer.collect(usages, scope);
        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 Project project, @NotNull AnalysisScope scope,
            @NotNull Map<Module, PsiFile> modules) {
        Set<Module> modulesWithoutAnnotations = new HashSet<>();
        Set<Module> modulesWithLowVersion = new HashSet<>();
        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);
            }
            GradleBuildModel buildModel = GradleBuildModel.get(module);
            if (buildModel == null) {
                Logger.getInstance(InferSupportAnnotationsAction.class)
                        .warn("Unable to find Gradle build model for module " + module.getModuleFilePath());
                continue;
            }
            boolean dependencyFound = false;
            DependenciesModel dependenciesModel = buildModel.dependencies();
            if (dependenciesModel != null) {
                for (ArtifactDependencyModel dependency : dependenciesModel.artifacts(COMPILE)) {
                    String notation = dependency.compactNotation().value();
                    if (notation.startsWith(SdkConstants.APPCOMPAT_LIB_ARTIFACT)
                            || notation.startsWith(SdkConstants.SUPPORT_LIB_ARTIFACT)
                            || notation.startsWith(SdkConstants.ANNOTATIONS_LIB_ARTIFACT)) {
                        dependencyFound = true;
                        break;
                    }
                }
            }
            if (!dependencyFound) {
                modulesWithoutAnnotations.add(module);
            }
        }

        if (!modulesWithLowVersion.isEmpty()) {
            Messages.showErrorDialog(project,
                    String.format(
                            "Infer Support Annotations requires the project sdk level be set to %1$d or greater.",
                            MIN_SDK_WITH_NULLABLE),
                    "Infer Support Annotations");
            return false;
        }
        if (modulesWithoutAnnotations.isEmpty()) {
            return true;
        }
        String moduleNames = StringUtil.join(modulesWithoutAnnotations, 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",
                SupportLibrary.SUPPORT_ANNOTATIONS.getArtifactId(), pluralize("dependency", count));
        if (Messages.showOkCancelDialog(project, message, "Infer Nullity Annotations",
                Messages.getErrorIcon()) == Messages.OK) {
            LocalHistoryAction action = LocalHistory.getInstance().startAction(ADD_DEPENDENCY);
            try {
                new WriteCommandAction(project, ADD_DEPENDENCY) {
                    @Override
                    protected void run(@NotNull Result result) throws Throwable {
                        RepositoryUrlManager manager = RepositoryUrlManager.get();
                        String annotationsLibraryCoordinate = manager
                                .getLibraryStringCoordinate(SupportLibrary.SUPPORT_ANNOTATIONS, true);
                        for (Module module : modulesWithoutAnnotations) {
                            addDependency(module, annotationsLibraryCoordinate);
                        }
                        GradleSyncInvoker.Request request = new GradleSyncInvoker.Request()
                                .setGenerateSourcesOnSuccess(false);
                        GradleSyncInvoker.getInstance().requestProjectSync(project, request,
                                new GradleSyncListener.Adapter() {
                                    @Override
                                    public void syncSucceeded(@NotNull Project project) {
                                        restartAnalysis(project, scope);
                                    }
                                });
                    }
                }.execute();
            } finally {
                action.finish();
            }
        }
        return false;
    }

    private static Runnable applyRunnable(Project project, Computable<UsageInfo[]> computable) {
        return () -> {
            LocalHistoryAction action = LocalHistory.getInstance().startAction(INFER_SUPPORT_ANNOTATIONS);
            try {
                new WriteCommandAction(project, INFER_SUPPORT_ANNOTATIONS) {
                    @Override
                    protected void run(@NotNull Result result) throws Throwable {
                        UsageInfo[] infos = computable.compute();
                        if (infos.length > 0) {

                            Set<PsiElement> elements = new LinkedHashSet<>();
                            for (UsageInfo info : infos) {
                                PsiElement element = info.getElement();
                                if (element != null) {
                                    PsiFile containingFile = element.getContainingFile();
                                    // Skip results in .class files; these are typically from extracted AAR files
                                    VirtualFile virtualFile = containingFile.getVirtualFile();
                                    if (virtualFile.getFileType().isBinary()) {
                                        continue;
                                    }

                                    ContainerUtil.addIfNotNull(elements, containingFile);
                                }
                            }
                            if (!FileModificationService.getInstance().preparePsiElementsForWrite(elements))
                                return;

                            SequentialModalProgressTask progressTask = new SequentialModalProgressTask(project,
                                    INFER_SUPPORT_ANNOTATIONS, false);
                            progressTask.setMinIterationTime(200);
                            progressTask.setTask(new AnnotateTask(project, progressTask, infos));
                            ProgressManager.getInstance().run(progressTask);
                        } else {
                            InferSupportAnnotations.nothingFoundMessage(project);
                        }
                    }
                }.execute();
            } finally {
                action.finish();
            }
        };
    }

    private void restartAnalysis(Project project, AnalysisScope scope) {
        ApplicationManager.getApplication().invokeLater(() -> analyze(project, scope));
    }

    private static void showUsageView(@NotNull Project project, UsageInfo[] usageInfos,
            @NotNull AnalysisScope scope) {
        UsageTarget[] targets = UsageTarget.EMPTY_ARRAY;
        Ref<Usage[]> convertUsagesRef = new Ref<>();
        if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(
                () -> ApplicationManager.getApplication()
                        .runReadAction(() -> convertUsagesRef.set(UsageInfo2UsageAdapter.convert(usageInfos))),
                "Preprocess Usages", true, project)) {
            return;
        }

        if (convertUsagesRef.isNull())
            return;
        Usage[] usages = convertUsagesRef.get();

        UsageViewPresentation presentation = new UsageViewPresentation();
        presentation.setTabText("Infer Nullity Preview");
        presentation.setShowReadOnlyStatusAsRed(true);
        presentation.setShowCancelButton(true);
        presentation.setUsagesString(RefactoringBundle.message("usageView.usagesText"));

        UsageView usageView = UsageViewManager.getInstance(project).showUsages(targets, usages, presentation,
                rerunFactory(project, scope));

        Runnable refactoringRunnable = applyRunnable(project, () -> {
            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_SUPPORT_ANNOTATIONS, canNotMakeString,
                INFER_SUPPORT_ANNOTATIONS, false);
    }

    @NotNull
    private static Factory<UsageSearcher> rerunFactory(@NotNull Project project, @NotNull AnalysisScope scope) {
        return () -> new UsageInfoSearcherAdapter() {
            @NotNull
            @Override
            protected UsageInfo[] findUsages() {
                return ObjectUtils.notNull(
                        InferSupportAnnotationsAction.findUsages(project, scope, scope.getFileCount()),
                        UsageInfo.EMPTY_ARRAY);
            }

            @Override
            public void generate(@NotNull Processor<Usage> processor) {
                processUsages(processor, project);
            }
        };
    }

    private static void addDependency(@NotNull Module module, @Nullable String libraryCoordinate) {
        if (isNotEmpty(libraryCoordinate)) {
            ModuleRootModificationUtil.updateModel(module, model -> {
                GradleBuildModel buildModel = GradleBuildModel.get(module);
                if (buildModel != null) {
                    buildModel.dependencies().addArtifact(COMPILE, libraryCoordinate);
                    buildModel.applyChanges();
                }
            });
        }
    }

    /* Android nullable annotations do not support annotations on local variables. */
    @Override
    protected JComponent getAdditionalActionSettings(Project project, BaseAnalysisActionDialog dialog) {
        if (!GradleProjectInfo.getInstance(project).isBuildWithGradle()) {
            return super.getAdditionalActionSettings(project, dialog);
        }
        return null;
    }

    private static class AnnotateTask implements SequentialTask {
        private final Project myProject;
        private UsageInfo[] myInfos;
        private final SequentialModalProgressTask myTask;
        private int myCount = 0;
        private final int myTotal;

        public AnnotateTask(Project project, SequentialModalProgressTask progressTask, UsageInfo[] infos) {
            myProject = project;
            myInfos = infos;
            myTask = progressTask;
            myTotal = infos.length;
        }

        @Override
        public void prepare() {
        }

        @Override
        public boolean isDone() {
            return myCount > myTotal - 1;
        }

        @Override
        public boolean iteration() {
            ProgressIndicator indicator = myTask.getIndicator();
            if (indicator != null) {
                indicator.setFraction(((double) myCount) / myTotal);
            }

            InferSupportAnnotations.apply(myProject, myInfos[myCount++]);

            boolean done = isDone();

            if (isDone()) {
                try {
                    showReport();
                } catch (Throwable ignore) {
                }
            }
            return done;
        }

        @Override
        public void stop() {
        }

        public void showReport() {
            if (InferSupportAnnotations.CREATE_INFERENCE_REPORT) {
                String report = InferSupportAnnotations.generateReport(myInfos);
                String fileName = "Annotation Inference Report";
                ScratchFileService.Option option = ScratchFileService.Option.create_new_always;
                VirtualFile f = ScratchRootType.getInstance().createScratchFile(myProject, fileName,
                        StdLanguages.TEXT, report, option);
                if (f != null) {
                    FileEditorManager.getInstance(myProject).openFile(f, true);
                }
            }
        }
    }
}