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