summaryrefslogtreecommitdiff
path: root/java/java-impl/src/com/intellij/codeInspection/inferNullity/InferNullityAnnotationsAction.java
blob: df49b83ff48d400813cbf0aae28184d43a9fd4e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
/*
 * 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;
  }
}