blob: 0b73adc3fa4c1a99f56d55327c7ef06601e75655 [file] [log] [blame]
/*
* Copyright 2000-2013 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.jetbrains.python.inspections;
import com.google.common.collect.ImmutableSet;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.ui.ListEditForm;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.JDOMExternalizableStringList;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.profile.codeInspection.InspectionProfileManager;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.util.Function;
import com.jetbrains.python.codeInsight.stdlib.PyStdlibUtil;
import com.jetbrains.python.packaging.*;
import com.jetbrains.python.packaging.ui.PyChooseRequirementsDialog;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.sdk.PySdkUtil;
import com.jetbrains.python.sdk.PythonSdkType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
/**
* @author vlan
*/
public class PyPackageRequirementsInspection extends PyInspection {
public JDOMExternalizableStringList ignoredPackages = new JDOMExternalizableStringList();
@NotNull
@Override
public String getDisplayName() {
return "Package requirements";
}
@Override
public JComponent createOptionsPanel() {
final ListEditForm form = new ListEditForm("Ignore packages", ignoredPackages);
return form.getContentPanel();
}
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
boolean isOnTheFly,
@NotNull LocalInspectionToolSession session) {
return new Visitor(holder, session, ignoredPackages);
}
@Nullable
public static PyPackageRequirementsInspection getInstance(@NotNull PsiElement element) {
final InspectionProfile inspectionProfile = InspectionProjectProfileManager.getInstance(element.getProject()).getInspectionProfile();
final String toolName = PyPackageRequirementsInspection.class.getSimpleName();
return (PyPackageRequirementsInspection)inspectionProfile.getUnwrappedTool(toolName, element);
}
private static class Visitor extends PyInspectionVisitor {
private final Set<String> myIgnoredPackages;
public Visitor(@Nullable ProblemsHolder holder, @NotNull LocalInspectionToolSession session, Collection<String> ignoredPackages) {
super(holder, session);
myIgnoredPackages = ImmutableSet.copyOf(ignoredPackages);
}
@Override
public void visitPyFile(PyFile node) {
final Module module = ModuleUtilCore.findModuleForPsiElement(node);
if (module != null) {
if (isRunningPackagingTasks(module)) {
return;
}
final Sdk sdk = PythonSdkType.findPythonSdk(module);
if (sdk != null) {
final List<PyRequirement> unsatisfied = findUnsatisfiedRequirements(module, sdk, myIgnoredPackages);
if (unsatisfied != null && !unsatisfied.isEmpty()) {
final boolean plural = unsatisfied.size() > 1;
String msg = String.format("Package requirement%s %s %s not satisfied",
plural ? "s" : "",
requirementsToString(unsatisfied),
plural ? "are" : "is");
final Set<String> unsatisfiedNames = new HashSet<String>();
for (PyRequirement req : unsatisfied) {
unsatisfiedNames.add(req.getName());
}
final List<LocalQuickFix> quickFixes = new ArrayList<LocalQuickFix>();
quickFixes.add(new PyInstallRequirementsFix(null, module, sdk, unsatisfied));
quickFixes.add(new IgnoreRequirementFix(unsatisfiedNames));
registerProblem(node, msg,
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, null,
quickFixes.toArray(new LocalQuickFix[quickFixes.size()]));
}
}
}
}
@Override
public void visitPyFromImportStatement(PyFromImportStatement node) {
final PyReferenceExpression expr = node.getImportSource();
if (expr != null) {
checkPackageNameInRequirements(expr);
}
}
@Override
public void visitPyImportStatement(PyImportStatement node) {
for (PyImportElement element : node.getImportElements()) {
final PyReferenceExpression expr = element.getImportReferenceExpression();
if (expr != null) {
checkPackageNameInRequirements(expr);
}
}
}
private void checkPackageNameInRequirements(@NotNull PyQualifiedExpression importedExpression) {
for (PyInspectionExtension extension : Extensions.getExtensions(PyInspectionExtension.EP_NAME)) {
if (extension.ignorePackageNameInRequirements(importedExpression)) {
return;
}
}
final PyExpression packageReferenceExpression = PyPsiUtils.getFirstQualifier(importedExpression);
if (packageReferenceExpression != null) {
final String packageName = packageReferenceExpression.getName();
if (packageName != null && !myIgnoredPackages.contains(packageName)) {
if (!ApplicationManager.getApplication().isUnitTestMode() && !PyPIPackageUtil.INSTANCE.isInPyPI(packageName)) {
return;
}
final Collection<String> stdlibPackages = PyStdlibUtil.getPackages();
if (stdlibPackages != null) {
if (stdlibPackages.contains(packageName)) {
return;
}
}
if (PyPackageManager.PACKAGE_SETUPTOOLS.equals(packageName)) {
return;
}
final Module module = ModuleUtilCore.findModuleForPsiElement(packageReferenceExpression);
if (module != null) {
final Sdk sdk = PythonSdkType.findPythonSdk(module);
if (sdk != null) {
final PyPackageManager manager = PyPackageManager.getInstance(sdk);
Collection<PyRequirement> requirements = manager.getRequirements(module);
if (requirements != null) {
requirements = getTransitiveRequirements(sdk, requirements, new HashSet<PyPackage>());
}
if (requirements == null) return;
for (PyRequirement req : requirements) {
if (packageName.equalsIgnoreCase(req.getName())) {
return;
}
}
final PsiReference reference = packageReferenceExpression.getReference();
if (reference != null) {
final PsiElement element = reference.resolve();
if (element != null) {
final PsiFile file = element.getContainingFile();
if (file != null) {
final VirtualFile virtualFile = file.getVirtualFile();
if (ModuleUtilCore.moduleContainsFile(module, virtualFile, false)) {
return;
}
}
}
}
final List<LocalQuickFix> quickFixes = new ArrayList<LocalQuickFix>();
quickFixes.add(new AddToRequirementsFix(module, packageName, LanguageLevel.forElement(importedExpression)));
quickFixes.add(new IgnoreRequirementFix(Collections.singleton(packageName)));
registerProblem(packageReferenceExpression, String.format("Package '%s' is not listed in project requirements", packageName),
ProblemHighlightType.WEAK_WARNING, null,
quickFixes.toArray(new LocalQuickFix[quickFixes.size()]));
}
}
}
}
}
}
@Nullable
private static Set<PyRequirement> getTransitiveRequirements(@NotNull Sdk sdk, @NotNull Collection<PyRequirement> requirements,
@NotNull Set<PyPackage> visited) {
final Set<PyRequirement> results = new HashSet<PyRequirement>(requirements);
final List<PyPackage> packages;
try {
packages = PyPackageManager.getInstance(sdk).getPackages(PySdkUtil.isRemote(sdk));
}
catch (PyExternalProcessException e) {
return null;
}
if (packages == null) return null;
for (PyRequirement req : requirements) {
final PyPackage pkg = req.match(packages);
if (pkg != null && !visited.contains(pkg)) {
visited.add(pkg);
final Set<PyRequirement> transitive = getTransitiveRequirements(sdk, pkg.getRequirements(), visited);
if (transitive == null) return null;
results.addAll(transitive);
}
}
return results;
}
@NotNull
private static String requirementsToString(@NotNull List<PyRequirement> requirements) {
return StringUtil.join(requirements, new Function<PyRequirement, String>() {
@Override
public String fun(PyRequirement requirement) {
return String.format("'%s'", requirement.toString());
}
}, ", ");
}
@Nullable
private static List<PyRequirement> findUnsatisfiedRequirements(@NotNull Module module, @NotNull Sdk sdk,
@NotNull Set<String> ignoredPackages) {
final PyPackageManager manager = PyPackageManager.getInstance(sdk);
List<PyRequirement> requirements = manager.getRequirements(module);
if (requirements != null) {
final List<PyPackage> packages;
try {
packages = manager.getPackages(PySdkUtil.isRemote(sdk));
}
catch (PyExternalProcessException e) {
return null;
}
if (packages == null) return null;
final List<PyRequirement> unsatisfied = new ArrayList<PyRequirement>();
for (PyRequirement req : requirements) {
if (!ignoredPackages.contains(req.getName()) && req.match(packages) == null) {
unsatisfied.add(req);
}
}
return unsatisfied;
}
return null;
}
private static void setRunningPackagingTasks(@NotNull Module module, boolean value) {
module.putUserData(PyPackageManager.RUNNING_PACKAGING_TASKS, value);
}
private static boolean isRunningPackagingTasks(@NotNull Module module) {
final Boolean value = module.getUserData(PyPackageManager.RUNNING_PACKAGING_TASKS);
return value != null && value;
}
public static class PyInstallRequirementsFix implements LocalQuickFix {
@NotNull private String myName;
@NotNull private final Module myModule;
@NotNull private Sdk mySdk;
@NotNull private final List<PyRequirement> myUnsatisfied;
public PyInstallRequirementsFix(@Nullable String name, @NotNull Module module, @NotNull Sdk sdk,
@NotNull List<PyRequirement> unsatisfied) {
final boolean plural = unsatisfied.size() > 1;
myName = name != null ? name : String.format("Install requirement%s", plural ? "s" : "");
myModule = module;
mySdk = sdk;
myUnsatisfied = unsatisfied;
}
@NotNull
@Override
public String getName() {
return myName;
}
@NotNull
@Override
public String getFamilyName() {
return myName;
}
@Override
public void applyFix(@NotNull final Project project, @NotNull ProblemDescriptor descriptor) {
boolean installManagement = false;
final PyPackageManager manager = PyPackageManager.getInstance(mySdk);
if (!manager.hasManagement(false)) {
final int result = Messages.showYesNoDialog(project,
"Python packaging tools are required for installing packages. Do you want to " +
"install 'pip' and 'setuptools' for your interpreter?",
"Install Python Packaging Tools",
Messages.getQuestionIcon());
if (result == Messages.YES) {
installManagement = true;
}
else {
return;
}
}
final List<PyRequirement> chosen;
if (myUnsatisfied.size() > 1) {
final PyChooseRequirementsDialog dialog = new PyChooseRequirementsDialog(project, myUnsatisfied);
chosen = dialog.showAndGetResult();
}
else {
chosen = myUnsatisfied;
}
if (chosen.isEmpty()) {
return;
}
if (installManagement) {
final PyPackageManagerUI ui = new PyPackageManagerUI(project, mySdk, new UIListener(myModule) {
@Override
public void finished(List<PyExternalProcessException> exceptions) {
super.finished(exceptions);
if (exceptions.isEmpty()) {
installRequirements(project, chosen);
}
}
});
ui.installManagement();
}
else {
installRequirements(project, chosen);
}
}
private void installRequirements(Project project, List<PyRequirement> requirements) {
final PyPackageManagerUI ui = new PyPackageManagerUI(project, mySdk, new UIListener(myModule));
ui.install(requirements, Collections.<String>emptyList());
}
}
private static class UIListener implements PyPackageManagerUI.Listener {
private final Module myModule;
public UIListener(Module module) {
myModule = module;
}
@Override
public void started() {
setRunningPackagingTasks(myModule, true);
}
@Override
public void finished(List<PyExternalProcessException> exceptions) {
setRunningPackagingTasks(myModule, false);
}
}
private static class IgnoreRequirementFix implements LocalQuickFix {
@NotNull private final Set<String> myPackageNames;
public IgnoreRequirementFix(@NotNull Set<String> packageNames) {
myPackageNames = packageNames;
}
@NotNull
@Override
public String getName() {
final boolean plural = myPackageNames.size() > 1;
return String.format("Ignore requirement%s", plural ? "s" : "");
}
@NotNull
@Override
public String getFamilyName() {
return getName();
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement();
if (element != null) {
final PyPackageRequirementsInspection inspection = getInstance(element);
if (inspection != null) {
final JDOMExternalizableStringList ignoredPackages = inspection.ignoredPackages;
boolean changed = false;
for (String name : myPackageNames) {
if (!ignoredPackages.contains(name)) {
ignoredPackages.add(name);
changed = true;
}
}
if (changed) {
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
InspectionProfileManager.getInstance().fireProfileChanged(profile);
}
}
}
}
}
private static class AddToRequirementsFix implements LocalQuickFix {
@NotNull private final String myPackageName;
@NotNull private final LanguageLevel myLanguageLevel;
@NotNull private Module myModule;
private AddToRequirementsFix(@NotNull Module module, @NotNull String packageName, @NotNull LanguageLevel languageLevel) {
myPackageName = packageName;
myLanguageLevel = languageLevel;
myModule = module;
}
@Nullable
private PyArgumentList findSetupArgumentList() {
final PyFile setupPy = PyPackageUtil.findSetupPy(myModule);
if (setupPy != null) {
final PyCallExpression setupCall = PyPackageUtil.findSetupCall(setupPy);
if (setupCall != null) {
return setupCall.getArgumentList();
}
}
return null;
}
@NotNull
@Override
public String getName() {
final String target;
final VirtualFile requirementsTxt = PyPackageUtil.findRequirementsTxt(myModule);
final PyListLiteralExpression setupPyRequires = PyPackageUtil.findSetupPyRequires(myModule);
if (requirementsTxt != null) {
target = requirementsTxt.getName();
}
else if (setupPyRequires != null || findSetupArgumentList() != null) {
target = "setup.py";
}
else {
target = "project requirements";
}
return String.format("Add requirement '%s' to %s", myPackageName, target);
}
@NotNull
@Override
public String getFamilyName() {
return getName();
}
@Override
public void applyFix(@NotNull final Project project, @NotNull ProblemDescriptor descriptor) {
final VirtualFile requirementsTxt = PyPackageUtil.findRequirementsTxt(myModule);
final PyListLiteralExpression setupPyRequires = PyPackageUtil.findSetupPyRequires(myModule);
CommandProcessor.getInstance().executeCommand(project, new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
if (requirementsTxt != null) {
if (requirementsTxt.isWritable()) {
final Document document = FileDocumentManager.getInstance().getDocument(requirementsTxt);
if (document != null) {
document.insertString(0, myPackageName + "\n");
}
}
}
else {
final PyElementGenerator generator = PyElementGenerator.getInstance(project);
final PyArgumentList argumentList = findSetupArgumentList();
if (setupPyRequires != null) {
if (setupPyRequires.getContainingFile().isWritable()) {
final String text = String.format("'%s'", myPackageName);
final PyExpression generated = generator.createExpressionFromText(myLanguageLevel, text);
setupPyRequires.add(generated);
}
}
else if (argumentList != null) {
final PyKeywordArgument requiresArg = generateRequiresKwarg(generator);
if (requiresArg != null) {
argumentList.addArgument(requiresArg);
}
}
}
}
@Nullable
private PyKeywordArgument generateRequiresKwarg(PyElementGenerator generator) {
final String text = String.format("foo(requires=['%s'])", myPackageName);
final PyExpression generated = generator.createExpressionFromText(myLanguageLevel, text);
PyKeywordArgument installRequiresArg = null;
if (generated instanceof PyCallExpression) {
final PyCallExpression foo = (PyCallExpression)generated;
for (PyExpression arg : foo.getArguments()) {
if (arg instanceof PyKeywordArgument) {
final PyKeywordArgument kwarg = (PyKeywordArgument)arg;
if ("requires".equals(kwarg.getKeyword())) {
installRequiresArg = kwarg;
}
}
}
}
return installRequiresArg;
}
});
}
}, getName(), null);
}
}
}