Merge remote-tracking branch 'origin/master' into develar/is
[idea/community.git] / python / src / com / jetbrains / python / inspections / PyPackageRequirementsInspection.java
1 /*
2  * Copyright 2000-2016 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 package com.jetbrains.python.inspections;
17
18 import com.google.common.collect.ImmutableSet;
19 import com.intellij.codeInspection.*;
20 import com.intellij.codeInspection.ui.ListEditForm;
21 import com.intellij.execution.ExecutionException;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.command.CommandProcessor;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.extensions.Extensions;
26 import com.intellij.openapi.module.Module;
27 import com.intellij.openapi.module.ModuleUtilCore;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.projectRoots.Sdk;
30 import com.intellij.openapi.ui.Messages;
31 import com.intellij.openapi.util.JDOMExternalizableStringList;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.profile.codeInspection.InspectionProfileManager;
34 import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
35 import com.intellij.psi.*;
36 import com.jetbrains.python.codeInsight.imports.AddImportHelper;
37 import com.jetbrains.python.codeInsight.stdlib.PyStdlibUtil;
38 import com.jetbrains.python.packaging.*;
39 import com.jetbrains.python.packaging.ui.PyChooseRequirementsDialog;
40 import com.jetbrains.python.psi.*;
41 import com.jetbrains.python.psi.impl.PyPsiUtils;
42 import com.jetbrains.python.sdk.PySdkUtil;
43 import com.jetbrains.python.sdk.PythonSdkType;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import javax.swing.*;
48 import java.util.*;
49
50 /**
51  * @author vlan
52  */
53 public class PyPackageRequirementsInspection extends PyInspection {
54   private static final Logger LOG = Logger.getInstance(PyPackageRequirementsInspection.class);
55
56   public JDOMExternalizableStringList ignoredPackages = new JDOMExternalizableStringList();
57
58   @NotNull
59   @Override
60   public String getDisplayName() {
61     return "Package requirements";
62   }
63
64   @Override
65   public JComponent createOptionsPanel() {
66     final ListEditForm form = new ListEditForm("Ignore packages", ignoredPackages);
67     return form.getContentPanel();
68   }
69
70   @NotNull
71   @Override
72   public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
73                                         boolean isOnTheFly,
74                                         @NotNull LocalInspectionToolSession session) {
75     return new Visitor(holder, session, ignoredPackages);
76   }
77
78   @Nullable
79   public static PyPackageRequirementsInspection getInstance(@NotNull PsiElement element) {
80     final InspectionProfile inspectionProfile = InspectionProjectProfileManager.getInstance(element.getProject()).getCurrentProfile();
81     final String toolName = PyPackageRequirementsInspection.class.getSimpleName();
82     return (PyPackageRequirementsInspection)inspectionProfile.getUnwrappedTool(toolName, element);
83   }
84
85   private static class Visitor extends PyInspectionVisitor {
86     private final Set<String> myIgnoredPackages;
87
88     public Visitor(@Nullable ProblemsHolder holder, @NotNull LocalInspectionToolSession session, Collection<String> ignoredPackages) {
89       super(holder, session);
90       myIgnoredPackages = ImmutableSet.copyOf(ignoredPackages);
91     }
92
93     @Override
94     public void visitPyFile(PyFile node) {
95       final Module module = ModuleUtilCore.findModuleForPsiElement(node);
96       if (module != null) {
97         if (isRunningPackagingTasks(module)) {
98           return;
99         }
100         final Sdk sdk = PythonSdkType.findPythonSdk(module);
101         if (sdk != null) {
102           final List<PyRequirement> unsatisfied = findUnsatisfiedRequirements(module, sdk, myIgnoredPackages);
103           if (unsatisfied != null && !unsatisfied.isEmpty()) {
104             final boolean plural = unsatisfied.size() > 1;
105             String msg = String.format("Package requirement%s %s %s not satisfied",
106                                        plural ? "s" : "",
107                                        PyPackageUtil.requirementsToString(unsatisfied),
108                                        plural ? "are" : "is");
109             final Set<String> unsatisfiedNames = new HashSet<String>();
110             for (PyRequirement req : unsatisfied) {
111               unsatisfiedNames.add(req.getFullName());
112             }
113             final List<LocalQuickFix> quickFixes = new ArrayList<LocalQuickFix>();
114             quickFixes.add(new PyInstallRequirementsFix(null, module, sdk, unsatisfied));
115             quickFixes.add(new IgnoreRequirementFix(unsatisfiedNames));
116             registerProblem(node, msg,
117                             ProblemHighlightType.GENERIC_ERROR_OR_WARNING, null,
118                             quickFixes.toArray(new LocalQuickFix[quickFixes.size()]));
119           }
120         }
121       }
122     }
123
124     @Override
125     public void visitPyFromImportStatement(PyFromImportStatement node) {
126       final PyReferenceExpression expr = node.getImportSource();
127       if (expr != null) {
128         checkPackageNameInRequirements(expr);
129       }
130     }
131
132     @Override
133     public void visitPyImportStatement(PyImportStatement node) {
134       for (PyImportElement element : node.getImportElements()) {
135         final PyReferenceExpression expr = element.getImportReferenceExpression();
136         if (expr != null) {
137           checkPackageNameInRequirements(expr);
138         }
139       }
140     }
141
142     private void checkPackageNameInRequirements(@NotNull PyQualifiedExpression importedExpression) {
143       for (PyInspectionExtension extension : Extensions.getExtensions(PyInspectionExtension.EP_NAME)) {
144         if (extension.ignorePackageNameInRequirements(importedExpression)) {
145           return;
146         }
147       }
148       final PyExpression packageReferenceExpression = PyPsiUtils.getFirstQualifier(importedExpression);
149       if (packageReferenceExpression != null) {
150         final String packageName = packageReferenceExpression.getName();
151         if (packageName != null && !myIgnoredPackages.contains(packageName)) {
152           if (!ApplicationManager.getApplication().isUnitTestMode() && !PyPIPackageUtil.INSTANCE.isInPyPI(packageName)) {
153             return;
154           }
155           final Collection<String> stdlibPackages = PyStdlibUtil.getPackages();
156           if (stdlibPackages != null) {
157             if (stdlibPackages.contains(packageName)) {
158               return;
159             }
160           }
161           if (PyPackageManager.SETUPTOOLS.equals(packageName)) {
162             return;
163           }
164           final Module module = ModuleUtilCore.findModuleForPsiElement(packageReferenceExpression);
165           if (module != null) {
166             final Sdk sdk = PythonSdkType.findPythonSdk(module);
167             if (sdk != null) {
168               final PyPackageManager manager = PyPackageManager.getInstance(sdk);
169               Collection<PyRequirement> requirements = manager.getRequirements(module);
170               if (requirements != null) {
171                 requirements = getTransitiveRequirements(sdk, requirements, new HashSet<PyPackage>());
172               }
173               if (requirements == null) return;
174               for (PyRequirement req : requirements) {
175                 if (packageName.equalsIgnoreCase(req.getName())) {
176                   return;
177                 }
178               }
179               if (!ApplicationManager.getApplication().isUnitTestMode()) {
180                 final PsiReference reference = packageReferenceExpression.getReference();
181                 if (reference != null) {
182                   final PsiElement element = reference.resolve();
183                   if (element != null) {
184                     final PsiFile file = element.getContainingFile();
185                     if (file != null) {
186                       final VirtualFile virtualFile = file.getVirtualFile();
187                       if (ModuleUtilCore.moduleContainsFile(module, virtualFile, false)) {
188                         return;
189                       }
190                     }
191                   }
192                 }
193               }
194               final List<LocalQuickFix> quickFixes = new ArrayList<LocalQuickFix>();
195               quickFixes.add(new AddToRequirementsFix(module, packageName, LanguageLevel.forElement(importedExpression)));
196               quickFixes.add(new IgnoreRequirementFix(Collections.singleton(packageName)));
197               registerProblem(packageReferenceExpression, String.format("Package '%s' is not listed in project requirements", packageName),
198                               ProblemHighlightType.WEAK_WARNING, null,
199                               quickFixes.toArray(new LocalQuickFix[quickFixes.size()]));
200             }
201           }
202         }
203       }
204     }
205   }
206
207   @Nullable
208   private static Set<PyRequirement> getTransitiveRequirements(@NotNull Sdk sdk, @NotNull Collection<PyRequirement> requirements,
209                                                               @NotNull Set<PyPackage> visited) {
210     if (requirements.isEmpty()) {
211       return Collections.emptySet();
212     }
213     final Set<PyRequirement> results = new HashSet<PyRequirement>(requirements);
214     final List<PyPackage> packages;
215     try {
216       packages = PyPackageManager.getInstance(sdk).getPackages(PySdkUtil.isRemote(sdk));
217     }
218     catch (ExecutionException e) {
219       return null;
220     }
221     if (packages == null) return null;
222     for (PyRequirement req : requirements) {
223       final PyPackage pkg = req.match(packages);
224       if (pkg != null && !visited.contains(pkg)) {
225         visited.add(pkg);
226         final Set<PyRequirement> transitive = getTransitiveRequirements(sdk, pkg.getRequirements(), visited);
227         if (transitive == null) return null;
228         results.addAll(transitive);
229       }
230     }
231     return results;
232   }
233
234   @Nullable
235   private static List<PyRequirement> findUnsatisfiedRequirements(@NotNull Module module, @NotNull Sdk sdk,
236                                                                  @NotNull Set<String> ignoredPackages) {
237     final PyPackageManager manager = PyPackageManager.getInstance(sdk);
238     List<PyRequirement> requirements = manager.getRequirements(module);
239     if (requirements != null) {
240       final List<PyPackage> packages;
241       try {
242         packages = manager.getPackages(PySdkUtil.isRemote(sdk));
243       }
244       catch (ExecutionException e) {
245         LOG.warn(e);
246         return null;
247       }
248       if (packages == null) return null;
249       final List<PyRequirement> unsatisfied = new ArrayList<PyRequirement>();
250       for (PyRequirement req : requirements) {
251         if (!ignoredPackages.contains(req.getName()) && req.match(packages) == null) {
252           unsatisfied.add(req);
253         }
254       }
255       return unsatisfied;
256     }
257     return null;
258   }
259
260   private static void setRunningPackagingTasks(@NotNull Module module, boolean value) {
261     module.putUserData(PyPackageManager.RUNNING_PACKAGING_TASKS, value);
262   }
263
264   private static boolean isRunningPackagingTasks(@NotNull Module module) {
265     final Boolean value = module.getUserData(PyPackageManager.RUNNING_PACKAGING_TASKS);
266     return value != null && value;
267   }
268
269   public static class PyInstallRequirementsFix implements LocalQuickFix {
270     @NotNull private String myName;
271     @NotNull private final Module myModule;
272     @NotNull private Sdk mySdk;
273     @NotNull private final List<PyRequirement> myUnsatisfied;
274
275     public PyInstallRequirementsFix(@Nullable String name, @NotNull Module module, @NotNull Sdk sdk,
276                                     @NotNull List<PyRequirement> unsatisfied) {
277       final boolean plural = unsatisfied.size() > 1;
278       myName = name != null ? name : String.format("Install requirement%s", plural ? "s" : "");
279       myModule = module;
280       mySdk = sdk;
281       myUnsatisfied = unsatisfied;
282     }
283
284     @NotNull
285     @Override
286     public String getName() {
287       return myName;
288     }
289
290     @NotNull
291     @Override
292     public String getFamilyName() {
293       return myName;
294     }
295
296     @Override
297     public void applyFix(@NotNull final Project project, @NotNull ProblemDescriptor descriptor) {
298       boolean installManagement = false;
299       final PyPackageManager manager = PyPackageManager.getInstance(mySdk);
300       boolean hasManagement = false;
301       try {
302         hasManagement = manager.hasManagement(false);
303       }
304       catch (ExecutionException ignored) {
305       }
306       if (!hasManagement) {
307         final int result = Messages.showYesNoDialog(project,
308                                                     "Python packaging tools are required for installing packages. Do you want to " +
309                                                     "install 'pip' and 'setuptools' for your interpreter?",
310                                                     "Install Python Packaging Tools",
311                                                     Messages.getQuestionIcon());
312         if (result == Messages.YES) {
313           installManagement = true;
314         }
315         else {
316           return;
317         }
318       }
319       final List<PyRequirement> chosen;
320       if (myUnsatisfied.size() > 1) {
321         final PyChooseRequirementsDialog dialog = new PyChooseRequirementsDialog(project, myUnsatisfied);
322         if (dialog.showAndGet()) {
323           chosen = dialog.getMarkedElements();
324         }
325         else {
326           chosen = Collections.emptyList();
327         }
328       }
329       else {
330         chosen = myUnsatisfied;
331       }
332       if (chosen.isEmpty()) {
333         return;
334       }
335       if (installManagement) {
336         final PyPackageManagerUI ui = new PyPackageManagerUI(project, mySdk, new UIListener(myModule) {
337           @Override
338           public void finished(List<ExecutionException> exceptions) {
339             super.finished(exceptions);
340             if (exceptions.isEmpty()) {
341               installRequirements(project, chosen);
342             }
343           }
344         });
345         ui.installManagement();
346       }
347       else {
348         installRequirements(project, chosen);
349       }
350     }
351
352     private void installRequirements(Project project, List<PyRequirement> requirements) {
353       final PyPackageManagerUI ui = new PyPackageManagerUI(project, mySdk, new UIListener(myModule));
354       ui.install(requirements, Collections.<String>emptyList());
355     }
356   }
357
358   public static class InstallAndImportQuickFix implements LocalQuickFix {
359
360     private final Sdk mySdk;
361     private final Module myModule;
362     private String myPackageName;
363     @Nullable private final String myAsName;
364     @NotNull private final SmartPsiElementPointer<PyElement> myNode;
365
366     public InstallAndImportQuickFix(@NotNull final String packageName,
367                                     @Nullable final String asName,
368                                     @NotNull final PyElement node) {
369       myPackageName = packageName;
370       myAsName = asName;
371       myNode = SmartPointerManager.getInstance(node.getProject()).createSmartPsiElementPointer(node, node.getContainingFile());
372       myModule = ModuleUtilCore.findModuleForPsiElement(node);
373       mySdk = PythonSdkType.findPythonSdk(myModule);
374     }
375
376     @NotNull
377     public String getName() {
378       return "Install and import package " + myPackageName;
379     }
380
381     @NotNull
382     public String getFamilyName() {
383       return "Install and import package " + myPackageName;
384     }
385
386     public void applyFix(@NotNull final Project project, @NotNull final ProblemDescriptor descriptor) {
387       final PyPackageManagerUI ui = new PyPackageManagerUI(project, mySdk, new UIListener(myModule) {
388         @Override
389         public void finished(List<ExecutionException> exceptions) {
390           super.finished(exceptions);
391           if (exceptions.isEmpty()) {
392
393             final PyElement element = myNode.getElement();
394             if (element == null) return;
395
396             CommandProcessor.getInstance().executeCommand(project, () -> ApplicationManager.getApplication().runWriteAction(() -> {
397               AddImportHelper.addImportStatement(element.getContainingFile(), myPackageName, myAsName,
398                                                  AddImportHelper.ImportPriority.THIRD_PARTY, element);
399             }), "Add import", "Add import");
400           }
401         }
402       });
403       ui.install(Collections.singletonList(new PyRequirement(myPackageName)), Collections.<String>emptyList());
404     }
405   }
406
407   private static class UIListener implements PyPackageManagerUI.Listener {
408     private final Module myModule;
409
410     public UIListener(Module module) {
411       myModule = module;
412     }
413
414     @Override
415     public void started() {
416       setRunningPackagingTasks(myModule, true);
417     }
418
419     @Override
420     public void finished(List<ExecutionException> exceptions) {
421       setRunningPackagingTasks(myModule, false);
422     }
423   }
424
425
426   private static class IgnoreRequirementFix implements LocalQuickFix {
427     @NotNull private final Set<String> myPackageNames;
428
429     public IgnoreRequirementFix(@NotNull Set<String> packageNames) {
430       myPackageNames = packageNames;
431     }
432
433     @NotNull
434     @Override
435     public String getName() {
436       final boolean plural = myPackageNames.size() > 1;
437       return String.format("Ignore requirement%s", plural ? "s" : "");
438     }
439
440     @NotNull
441     @Override
442     public String getFamilyName() {
443       return getName();
444     }
445
446     @Override
447     public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
448       final PsiElement element = descriptor.getPsiElement();
449       if (element != null) {
450         final PyPackageRequirementsInspection inspection = getInstance(element);
451         if (inspection != null) {
452           final JDOMExternalizableStringList ignoredPackages = inspection.ignoredPackages;
453           boolean changed = false;
454           for (String name : myPackageNames) {
455             if (!ignoredPackages.contains(name)) {
456               ignoredPackages.add(name);
457               changed = true;
458             }
459           }
460           if (changed) {
461             final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getCurrentProfile();
462             InspectionProfileManager.getInstance().fireProfileChanged(profile);
463           }
464         }
465       }
466     }
467   }
468
469   private static class AddToRequirementsFix implements LocalQuickFix {
470     @NotNull private final Module myModule;
471     @NotNull private final String myPackageName;
472     @NotNull private final LanguageLevel myLanguageLevel;
473
474     private AddToRequirementsFix(@NotNull Module module, @NotNull String packageName, @NotNull LanguageLevel languageLevel) {
475       myModule = module;
476       myPackageName = packageName;
477       myLanguageLevel = languageLevel;
478     }
479
480     @NotNull
481     @Override
482     public String getName() {
483       return String.format("Add requirement '%s' to %s", myPackageName, calculateTarget());
484     }
485
486     @NotNull
487     @Override
488     public String getFamilyName() {
489       return getName();
490     }
491
492     @Override
493     public void applyFix(@NotNull final Project project, @NotNull ProblemDescriptor descriptor) {
494       CommandProcessor.getInstance().executeCommand(project, () -> ApplicationManager.getApplication().runWriteAction(() -> {
495         PyPackageUtil.addRequirementToTxtOrSetupPy(myModule, myPackageName, myLanguageLevel);
496       }), getName(), null);
497     }
498
499     @NotNull
500     private String calculateTarget() {
501       final VirtualFile requirementsTxt = PyPackageUtil.findRequirementsTxt(myModule);
502       if (requirementsTxt != null) {
503         return requirementsTxt.getName();
504       }
505       else if (PyPackageUtil.findSetupCall(myModule) != null) {
506         return "setup.py";
507       }
508       else {
509         return "project requirements";
510       }
511     }
512   }
513 }